From 9b50dc515a8f2c864875076cf33d1e1023344437 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Wed, 16 Nov 2016 22:43:46 -0500 Subject: [PATCH 01/40] Added partition function --- utils.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/utils.py b/utils.py index 4ef7e0c08..16c94fb15 100644 --- a/utils.py +++ b/utils.py @@ -13,8 +13,20 @@ # 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." + """Coerce iterable to sequence, if it is not already one.""" return (iterable if isinstance(iterable, collections.abc.Sequence) else tuple(iterable)) @@ -46,7 +58,7 @@ def product(numbers): def first(iterable, default=None): - "Return the first element of an iterable or the next element of a generator; or default." + """Return the first element of an iterable or the next element of a generator; or default.""" try: return iterable[0] except IndexError: @@ -74,22 +86,19 @@ def argmin_random_tie(seq, key=identity): def argmax_random_tie(seq, key=identity): - "Return an element with highest fn(seq[i]) score; break ties at random." + """Return an element with highest fn(seq[i]) score; break ties at random.""" return argmax(shuffled(seq), key=key) def shuffled(iterable): - "Randomly shuffle a copy of iterable." + """Randomly shuffle a copy of iterable.""" items = list(iterable) random.shuffle(items) return items - # ______________________________________________________________________________ # Statistical and mathematical functions - - def histogram(values, mode=0, bin_function=None): """Return a list of (value, count) pairs, summarizing the input values. Sorted by increasing value, or if mode=1, by decreasing count. @@ -162,7 +171,6 @@ def vector_add(a, b): return tuple(map(operator.add, a, b)) - def scalar_vector_product(X, Y): """Return vector as a product of a scalar and a vector""" return [X * y for y in Y] From 401b49d6880198504607fea3fcd33d23658ea591 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Wed, 16 Nov 2016 22:49:26 -0500 Subject: [PATCH 02/40] New Action, PlanningKB and PlanningProblem classes to implement total-order planning. Also included air cargo, spare_tire, blocks world and sussman anomaly examples. --- planning.py | 499 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 299 insertions(+), 200 deletions(-) diff --git a/planning.py b/planning.py index 2dd57787a..731ce709d 100644 --- a/planning.py +++ b/planning.py @@ -1,38 +1,117 @@ """Planning (Chapters 10-11) """ +import copy +from logic import fol_bc_ask, fol_bc_and, variables +from utils import expr, Expr, partition, first +from search import Problem, astar_search + + +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 = [] + self.goal_clauses = frozenset(goals) + self.clause_set = frozenset(initial_clauses) + + 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 __ne__(self, other): + """__ne__ is easy to implement in terms of __eq__ for completeness.""" + return not self.__eq__(other) + + 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 + + # heuristic is just whether remaining unresolved goals in the current KB are less than the remaining unsolved + # goals in 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.""" + return hash(self.clause_set) + + def __repr__(self): + return '{}({}, {})'.format(self.__class__.__name__, list(self.goal_clauses), list(self.clause_set)) + + def ask(self, query): + """Return a substitution that makes the query true, or, failing that, return False.""" + return first(self.ask_generator(query), default=False) + + def ask_generator(self, query): + """Yield all the substitutions that make query true.""" + if not variables(query): + if query in self.clause_set: + for arg in query.args: + yield {arg: arg} + else: + for item in fol_bc_ask(self, query): + yield item + + def tell(self, sentence): + """ KB can't be altered since its state is frozen after __init__ """ + raise NotImplementedError + + def retract(self, sentence): + """ KB can't be altered since its state is frozen after __init__ """ + raise NotImplementedError -from utils import Expr, expr, first -from logic import FolKB + 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): + """ Returns: number of remaining goal clauses to be satisfied """ + return len(self.goal_clauses - self.clause_set) + + def fetch_rules_for_goal(self, goal): + return self.clause_set -class PDLL: +class PlanningProblem(Problem): """ - PDLL used to define a search problem - It stores states in a knowledge base consisting of first order logic statements - The conjunction of these logical statements completely define a state + Used to define a planning problem. + It stores states in a knowledge base consisting of first order logic statements. + The conjunction of these logical statements completely define a state. """ + def __init__(self, initial_state, actions, goals): + super().__init__(initial_state, goals) + self.action_list = actions - def __init__(self, initial_state, actions, goal_test): - self.kb = FolKB(initial_state) - self.actions = actions - self.goal_test_func = goal_test + def __repr__(self): + return '{}({}, {}, {})'.format(self.__class__.__name__, self.initial, self.action_list, self.goal) + + def actions(self, state): + for action in self.action_list: + for subst in action.check_precond(state): + new_action = copy.deepcopy(action) + new_action.subst = subst + 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 value(self, state): + """For optimization problems, each state has a value. Hill-climbing + and related algorithms try to maximize this value.""" + raise NotImplementedError - def goal_test(self): - return self.goal_test_func(self.kb) - - def act(self, action): - """ - Performs the action given as argument - Note that action is an Expr like expr('Remove(Glass, Table)') or expr('Eat(Sandwich)') - """ - action_name = action.op - args = action.args - list_action = first(a for a in self.actions if a.name == action_name) - if list_action is None: - raise Exception("Action '{}' not found".format(action_name)) - if not list_action.check_precond(self.kb, args): - raise Exception("Action '{}' pre-conditions not satisfied".format(action)) - list_action(self.kb, args) class Action: """ @@ -41,198 +120,218 @@ class Action: action is an Expr where variables are given as arguments(args) Precondition and effect are both lists with positive and negated literals Example: - precond_pos = [expr("Human(person)"), expr("Hungry(Person)")] - precond_neg = [expr("Eaten(food)")] - effect_add = [expr("Eaten(food)")] - effect_rem = [expr("Hungry(person)")] - eat = Action(expr("Eat(person, food)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + 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, action, precond, effect): - self.name = action.op - self.args = action.args - self.precond_pos = precond[0] - self.precond_neg = precond[1] - self.effect_add = effect[0] - self.effect_rem = effect[1] - - def __call__(self, kb, args): - return self.act(kb, args) - - def substitute(self, e, args): - """Replaces variables in expression with their respective Propostional symbol""" - new_args = list(e.args) - for num, x in enumerate(e.args): - for i in range(len(self.args)): - if self.args[i] == x: - new_args[num] = args[i] + def __init__(self, expression, precond, effect): + self.name = expression.op + self.args = expression.args + self.subst = None + + def is_negative_clause(e): + return e.op == '~' and len(e.args) == 1 + + precond_neg, precond_pos = partition(precond, 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 + effect_rem, effect_add = partition(effect, 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 + + def __repr__(self): + return 'Action({}, {}, {})'.format(Expr(self.name, self.args), + list(self.precond_pos) + ['~{0}'.format(p) for p in self.precond_neg], + list(self.effect_add) + ['~{0}'.format(e) for e in self.effect_rem]) + + 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_precond(self, kb, args): - """Checks if the precondition is satisfied in the current state""" - # check for positive clauses - for clause in self.precond_pos: - if self.substitute(clause, args) not in kb.clauses: - return False - # check for negative clauses - for clause in self.precond_neg: - if self.substitute(clause, args) in kb.clauses: - return False - return True - - def act(self, kb, args): + def check_neg_precond(self, kb, precond, subst): + for s in subst: + for _ in fol_bc_and(kb, list(precond), s): + # if any negative preconditions are satisfied by the substitution, then exit loop. + if precond: + break + else: + 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. + if clause_set.isdisjoint(neg_precond): + yield s + + def check_pos_precond(self, kb, precond, subst): + clause_set = kb.fetch_rules_for_goal(None) + 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? + if clause_set.issuperset(pos_precond): + yield s + + def check_precond(self, kb): + """Checks if preconditions are satisfied in the current state""" + yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) + + def act(self, subst, kb): + new_kb = PlanningKB(kb.goal_clauses, kb.clause_set) """Executes the action on the state's kb""" - # check if the preconditions are satisfied - if not self.check_precond(kb, args): - raise Exception("Action pre-conditions not satisfied") + clause_set = set(new_kb.clause_set) # remove negative literals for clause in self.effect_rem: - kb.retract(self.substitute(clause, args)) + subst_clause = self.substitute(subst, clause) + clause_set.discard(subst_clause) # add positive literals for clause in self.effect_add: - kb.tell(self.substitute(clause, args)) + subst_clause = self.substitute(subst, clause) + clause_set.add(subst_clause) + new_kb.clause_set = frozenset(clause_set) + return new_kb + + +def print_solution(node): + for action in node.solution(): + print(action.name, end='(') + for a in action.args[:-1]: + print('{},'.format(action.subst.get(a, a)), end=' ') + print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) def air_cargo(): - init = [expr('At(C1, SFO)'), - expr('At(C2, JFK)'), - expr('At(P1, SFO)'), - expr('At(P2, JFK)'), - expr('Cargo(C1)'), - expr('Cargo(C2)'), - expr('Plane(P1)'), - expr('Plane(P2)'), - expr('Airport(JFK)'), - expr('Airport(SFO)')] - - def goal_test(kb): - required = [expr('At(C1 , JFK)'), expr('At(C2 ,SFO)')] - for q in required: - if kb.ask(q) is False: - return False - return True - - ## Actions + goals = [expr('At(C1, JFK)'), expr('At(C2, SFO)')] + + init = PlanningKB(goals, + [expr('At(C1, SFO)'), + expr('At(C2, JFK)'), + expr('At(P1, SFO)'), + expr('At(P2, JFK)'), + expr('Cargo(C1)'), + expr('Cargo(C2)'), + expr('Plane(P1)'), + expr('Plane(P2)'), + expr('Airport(JFK)'), + expr('Airport(SFO)')]) + + # Actions # Load - precond_pos = [expr("At(c, a)"), expr("At(p, a)"), expr("Cargo(c)"), expr("Plane(p)"), expr("Airport(a)")] - precond_neg = [] - effect_add = [expr("In(c, p)")] - effect_rem = [expr("At(c, a)")] - load = Action(expr("Load(c, p, a)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + precond = [expr('At(c, a)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] + effect = [expr('In(c, p)'), expr('~At(c, a)')] + load = Action(expr('Load(c, p, a)'), precond, effect) # Unload - precond_pos = [expr("In(c, p)"), expr("At(p, a)"), expr("Cargo(c)"), expr("Plane(p)"), expr("Airport(a)")] - precond_neg = [] - effect_add = [expr("At(c, a)")] - effect_rem = [expr("In(c, p)")] - unload = Action(expr("Unload(c, p, a)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + precond = [expr('In(c, p)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] + effect = [expr('At(c, a)'), expr('~In(c, p)')] + unload = Action(expr('Unload(c, p, a)'), precond, effect) # Fly - # Used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function - precond_pos = [expr("At(p, f)"), expr("Plane(p)"), expr("Airport(f)"), expr("Airport(to)")] - precond_neg = [] - effect_add = [expr("At(p, to)")] - effect_rem = [expr("At(p, f)")] - fly = Action(expr("Fly(p, f, to)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function + precond = [expr('At(p, f)'), expr('Plane(p)'), expr('Airport(f)'), expr('Airport(to)')] + effect = [expr('At(p, to)'), expr('~At(p, f)')] + fly = Action(expr('Fly(p, f, to)'), precond, effect) - return PDLL(init, [load, unload, fly], goal_test) + p = PlanningProblem(init, [load, unload, fly], goals) + n = astar_search(p) + print_solution(n) def spare_tire(): - init = [expr('Tire(Flat)'), - expr('Tire(Spare)'), - expr('At(Flat, Axle)'), - expr('At(Spare, Trunk)')] - - def goal_test(kb): - required = [expr('At(Spare, Axle)'), expr('At(Flat, Ground)')] - for q in required: - if kb.ask(q) is False: - return False - return True - - ##Actions - #Remove - precond_pos = [expr("At(obj, loc)")] - precond_neg = [] - effect_add = [expr("At(obj, Ground)")] - effect_rem = [expr("At(obj, loc)")] - remove = Action(expr("Remove(obj, loc)"), [precond_pos, precond_neg], [effect_add, effect_rem]) - - #PutOn - precond_pos = [expr("Tire(t)"), expr("At(t, Ground)")] - precond_neg = [expr("At(Flat, Axle)")] - effect_add = [expr("At(t, Axle)")] - effect_rem = [expr("At(t, Ground)")] - put_on = Action(expr("PutOn(t, Axle)"), [precond_pos, precond_neg], [effect_add, effect_rem]) - - #LeaveOvernight - precond_pos = [] - precond_neg = [] - effect_add = [] - effect_rem = [expr("At(Spare, Ground)"), expr("At(Spare, Axle)"), expr("At(Spare, Trunk)"), - expr("At(Flat, Ground)"), expr("At(Flat, Axle)"), expr("At(Flat, Trunk)")] - leave_overnight = Action(expr("LeaveOvernight"), [precond_pos, precond_neg], [effect_add, effect_rem]) - - return PDLL(init, [remove, put_on, leave_overnight], goal_test) - -def three_block_tower(): - init = [expr('On(A, Table)'), - expr('On(B, Table)'), - expr('On(C, A)'), - expr('Block(A)'), - expr('Block(B)'), - expr('Block(C)'), - expr('Clear(B)'), - expr('Clear(C)')] - - def goal_test(kb): - required = [expr('On(A, B)'), expr('On(B, C)')] - for q in required: - if kb.ask(q) is False: - return False - return True - - ## Actions - # Move - precond_pos = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)'), expr('Block(y)')] - precond_neg = [] - effect_add = [expr('On(b, y)'), expr('Clear(x)')] - effect_rem = [expr('On(b, x)'), expr('Clear(y)')] - move = Action(expr('Move(b, x, y)'), [precond_pos, precond_neg], [effect_add, effect_rem]) - - # MoveToTable - precond_pos = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] - precond_neg = [] - effect_add = [expr('On(b, Table)'), expr('Clear(x)')] - effect_rem = [expr('On(b, x)')] - moveToTable = Action(expr('MoveToTable(b, x)'), [precond_pos, precond_neg], [effect_add, effect_rem]) - - return PDLL(init, [move, moveToTable], goal_test) - -def have_cake_and_eat_cake_too(): - init = [expr('Have(Cake)')] - - def goal_test(kb): - required = [expr('Have(Cake)'), expr('Eaten(Cake)')] - for q in required: - if kb.ask(q) is False: - return False - return True - - ##Actions - # Eat cake - precond_pos = [expr('Have(Cake)')] - precond_neg = [] - effect_add = [expr('Eaten(Cake)')] - effect_rem = [expr('Have(Cake)')] - eat_cake = Action(expr('Eat(Cake)'), [precond_pos, precond_neg], [effect_add, effect_rem]) - - #Bake Cake - precond_pos = [] - precond_neg = [expr('Have(Cake)')] - effect_add = [expr('Have(Cake)')] - effect_rem = [] - bake_cake = Action(expr('Bake(Cake)'), [precond_pos, precond_neg], [effect_add, effect_rem]) - - return PDLL(init, [eat_cake, bake_cake], goal_test) + goals = [expr('At(Spare, Axle)')] + + init = PlanningKB(goals, + [expr('At(Flat, Axle)'), + expr('At(Spare, Trunk)')]) + + # Actions + # Remove(Spare, Trunk) + precond = [expr('At(Spare, Trunk)')] + effect = [expr('At(Spare, Ground)'), expr('~At(Spare, Trunk)')] + remove_spare = Action(expr('Remove(Spare, Trunk)'), precond, effect) + + # Remove(Flat, Axle) + precond = [expr('At(Flat, Axle)')] + effect = [expr('At(Flat, Ground)'), expr('~At(Flat, Axle)')] + remove_flat = Action(expr('Remove(Flat, Axle)'), precond, effect) + + # PutOn(Spare, Axle) + precond = [expr('At(Spare, Ground)'), expr('~At(Flat, Axle)')] + effect = [expr('At(Spare, Axle)'), expr('~At(Spare, Ground)')] + put_on_spare = Action(expr('PutOn(Spare, Axle)'), precond, effect) + + # LeaveOvernight + precond = [] + effect = [expr('~At(Spare, Ground)'), expr('~At(Spare, Axle)'), expr('~At(Spare, Trunk)'), + expr('~At(Flat, Ground)'), expr('~At(Flat, Axle)')] + leave_overnight = Action(expr('LeaveOvernight'), precond, effect) + + p = PlanningProblem(init, [remove_spare, remove_flat, put_on_spare, leave_overnight], goals) + n = astar_search(p) + print_solution(n) + + +def blocks_world(): + goals = [expr('On(A, B)'), expr('On(B, C)')] + init = PlanningKB(goals, + [expr('On(A, Table)'), + expr('On(B, Table)'), + expr('On(C, Table)'), + expr('Block(A)'), + expr('Block(B)'), + expr('Block(C)'), + expr('Clear(A)'), + expr('Clear(B)'), + expr('Clear(C)')]) + + # Actions + # Move(b, x, y) + precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] + effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] + move = Action(expr('Move(b, x, y)'), precond, effect) + + # MoveToTable(b, x) + precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] + effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] + move_to_table = Action(expr('MoveToTable(b, x)'), precond, effect) + + p = PlanningProblem(init, [move, move_to_table], goals) + n = astar_search(p) + print_solution(n) + + +def sussman_anomaly(): + goals = [expr('On(A, B)'), expr('On(B, C)')] + init = PlanningKB(goals, + [expr('On(A, Table)'), + expr('On(B, Table)'), + expr('On(C, A)'), + expr('Block(A)'), + expr('Block(B)'), + expr('Block(C)'), + expr('Clear(B)'), + expr('Clear(C)')]) + + # Actions + # Move(b, x, y) + precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] + effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] + move = Action(expr('Move(b, x, y)'), precond, effect) + + # MoveToTable(b, x) + precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] + effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] + move_to_table = Action(expr('MoveToTable(b, x)'), precond, effect) + + p = PlanningProblem(init, [move, move_to_table], goals) + n = astar_search(p) + print_solution(n) + + +if __name__ == '__main__': + air_cargo() + print() + spare_tire() + print() + blocks_world() + print() + sussman_anomaly() From 2eb5360197b0feb9a578a93bc4a65afd87a7290e Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Wed, 16 Nov 2016 23:14:55 -0500 Subject: [PATCH 03/40] Removed extraneous methods from PlanningKB since it no longer derives from FolKB. Changed blocks_world function name to three_block_tower. Cleaned up imports and PlanningKB __repr__ function. --- planning.py | 40 +++++++--------------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/planning.py b/planning.py index 731ce709d..a159585ef 100644 --- a/planning.py +++ b/planning.py @@ -1,8 +1,8 @@ """Planning (Chapters 10-11) """ import copy -from logic import fol_bc_ask, fol_bc_and, variables -from utils import expr, Expr, partition, first +from logic import fol_bc_and +from utils import expr, Expr, partition from search import Problem, astar_search @@ -22,10 +22,6 @@ def __eq__(self, other): raise NotImplementedError return self.clause_set == other.clause_set - def __ne__(self, other): - """__ne__ is easy to implement in terms of __eq__ for completeness.""" - return not self.__eq__(other) - 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 @@ -44,28 +40,6 @@ def __hash__(self): def __repr__(self): return '{}({}, {})'.format(self.__class__.__name__, list(self.goal_clauses), list(self.clause_set)) - def ask(self, query): - """Return a substitution that makes the query true, or, failing that, return False.""" - return first(self.ask_generator(query), default=False) - - def ask_generator(self, query): - """Yield all the substitutions that make query true.""" - if not variables(query): - if query in self.clause_set: - for arg in query.args: - yield {arg: arg} - else: - for item in fol_bc_ask(self, query): - yield item - - def tell(self, sentence): - """ KB can't be altered since its state is frozen after __init__ """ - raise NotImplementedError - - def retract(self, sentence): - """ KB can't be altered since its state is frozen after __init__ """ - raise NotImplementedError - def goal_test(self): """ Goal is satisfied when KB at least contains all goal clauses. """ return self.clause_set >= self.goal_clauses @@ -141,9 +115,9 @@ def is_negative_clause(e): self.effect_rem = set(e.args[0] for e in effect_rem) # change the negative Exprs to positive def __repr__(self): - return 'Action({}, {}, {})'.format(Expr(self.name, self.args), - list(self.precond_pos) + ['~{0}'.format(p) for p in self.precond_neg], - list(self.effect_add) + ['~{0}'.format(e) for e in self.effect_rem]) + return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, self.args), + list(self.precond_pos) + ['~{0}'.format(p) for p in self.precond_neg], + list(self.effect_add) + ['~{0}'.format(e) for e in self.effect_rem]) def substitute(self, subst, e): """Replaces variables in expression with the same substitution used for the precondition. """ @@ -270,7 +244,7 @@ def spare_tire(): print_solution(n) -def blocks_world(): +def three_block_tower(): goals = [expr('On(A, B)'), expr('On(B, C)')] init = PlanningKB(goals, [expr('On(A, Table)'), @@ -332,6 +306,6 @@ def sussman_anomaly(): print() spare_tire() print() - blocks_world() + three_block_tower() print() sussman_anomaly() From e3cea7e8009957088443f609c81dabf352236553 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Wed, 16 Nov 2016 23:24:37 -0500 Subject: [PATCH 04/40] Cleaned up a comment. --- planning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/planning.py b/planning.py index a159585ef..c4c33cc14 100644 --- a/planning.py +++ b/planning.py @@ -29,8 +29,7 @@ def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplementedError - # heuristic is just whether remaining unresolved goals in the current KB are less than the remaining unsolved - # goals in the other KB. + # heuristic 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): From c78ea93ad130175132d8f93f8a4fe01a3aea68b4 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 19 Mar 2018 23:23:40 -0400 Subject: [PATCH 05/40] Cleaned up a comment. --- planning.py | 146 ++++++++++++++++++++++++++++++++++++++-------------- search.py | 21 +++++--- utils.py | 1 - 3 files changed, 123 insertions(+), 45 deletions(-) diff --git a/planning.py b/planning.py index c4c33cc14..5a44a9fc6 100644 --- a/planning.py +++ b/planning.py @@ -1,9 +1,10 @@ """Planning (Chapters 10-11) """ import copy -from logic import fol_bc_and +from logic import fol_bc_and, FolKB from utils import expr, Expr, partition -from search import Problem, astar_search +from search import Problem, astar_search, depth_first_tree_search, depth_first_graph_search +import timeit class PlanningKB: @@ -67,7 +68,7 @@ def __repr__(self): def actions(self, state): for action in self.action_list: for subst in action.check_precond(state): - new_action = copy.deepcopy(action) + new_action = action.copy() new_action.subst = subst yield new_action @@ -86,7 +87,7 @@ def value(self, state): raise NotImplementedError -class Action: +class PlanningAction: """ Defines an action schema using preconditions and effects Use this to describe actions in PDDL @@ -118,6 +119,18 @@ def __repr__(self): list(self.precond_pos) + ['~{0}'.format(p) for p in self.precond_neg], list(self.effect_add) + ['~{0}'.format(e) for e in self.effect_rem]) + def copy(self): + """Returns a copy of this object.""" + act = self.__new__(self.__class__, object) + 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] @@ -149,27 +162,44 @@ def check_precond(self, kb): yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) def act(self, subst, kb): + """ Executes the action on a new copy of the PlanningKB """ new_kb = PlanningKB(kb.goal_clauses, kb.clause_set) - """Executes the action on the state's kb""" clause_set = set(new_kb.clause_set) - # remove negative literals - for clause in self.effect_rem: - subst_clause = self.substitute(subst, clause) - clause_set.discard(subst_clause) - # add positive literals - for clause in self.effect_add: - subst_clause = self.substitute(subst, clause) - clause_set.add(subst_clause) - new_kb.clause_set = frozenset(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 +class POPPlan: + def __init__(self, initial_kb): + precond = [] + effect = initial_kb.clause_set + start = PlanningAction(expr('Start'), precond, effect) + + precond = initial_kb.goal_clauses + effect = [] + finish = PlanningAction(expr('Finish'), precond, effect) + + self.action_list = [start, finish] + self.constraints = {(start, finish)} + self.causal_links = set() + self.open_preconds = set(initial_kb.goal_clauses) + + def successors(self): + pass + + def print_solution(node): for action in node.solution(): print(action.name, end='(') for a in action.args[:-1]: print('{},'.format(action.subst.get(a, a)), end=' ') - print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) + if action.args: + print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) + else: + print(')') + print() def air_cargo(): @@ -191,22 +221,22 @@ def air_cargo(): # Load precond = [expr('At(c, a)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] effect = [expr('In(c, p)'), expr('~At(c, a)')] - load = Action(expr('Load(c, p, a)'), precond, effect) + load = PlanningAction(expr('Load(c, p, a)'), precond, effect) # Unload precond = [expr('In(c, p)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] effect = [expr('At(c, a)'), expr('~In(c, p)')] - unload = Action(expr('Unload(c, p, a)'), precond, effect) + unload = PlanningAction(expr('Unload(c, p, a)'), precond, effect) # Fly # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function precond = [expr('At(p, f)'), expr('Plane(p)'), expr('Airport(f)'), expr('Airport(to)')] effect = [expr('At(p, to)'), expr('~At(p, f)')] - fly = Action(expr('Fly(p, f, to)'), precond, effect) + fly = PlanningAction(expr('Fly(p, f, to)'), precond, effect) p = PlanningProblem(init, [load, unload, fly], goals) - n = astar_search(p) - print_solution(n) + for solution in astar_search(p): + print_solution(solution) def spare_tire(): @@ -220,27 +250,27 @@ def spare_tire(): # Remove(Spare, Trunk) precond = [expr('At(Spare, Trunk)')] effect = [expr('At(Spare, Ground)'), expr('~At(Spare, Trunk)')] - remove_spare = Action(expr('Remove(Spare, Trunk)'), precond, effect) + remove_spare = PlanningAction(expr('Remove(Spare, Trunk)'), precond, effect) # Remove(Flat, Axle) precond = [expr('At(Flat, Axle)')] effect = [expr('At(Flat, Ground)'), expr('~At(Flat, Axle)')] - remove_flat = Action(expr('Remove(Flat, Axle)'), precond, effect) + remove_flat = PlanningAction(expr('Remove(Flat, Axle)'), precond, effect) # PutOn(Spare, Axle) precond = [expr('At(Spare, Ground)'), expr('~At(Flat, Axle)')] effect = [expr('At(Spare, Axle)'), expr('~At(Spare, Ground)')] - put_on_spare = Action(expr('PutOn(Spare, Axle)'), precond, effect) + put_on_spare = PlanningAction(expr('PutOn(Spare, Axle)'), precond, effect) # LeaveOvernight precond = [] effect = [expr('~At(Spare, Ground)'), expr('~At(Spare, Axle)'), expr('~At(Spare, Trunk)'), expr('~At(Flat, Ground)'), expr('~At(Flat, Axle)')] - leave_overnight = Action(expr('LeaveOvernight'), precond, effect) + leave_overnight = PlanningAction(expr('LeaveOvernight'), precond, effect) p = PlanningProblem(init, [remove_spare, remove_flat, put_on_spare, leave_overnight], goals) - n = astar_search(p) - print_solution(n) + for s in astar_search(p): + print_solution(s) def three_block_tower(): @@ -260,16 +290,16 @@ def three_block_tower(): # Move(b, x, y) precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] - move = Action(expr('Move(b, x, y)'), precond, effect) + move = PlanningAction(expr('Move(b, x, y)'), precond, effect) # MoveToTable(b, x) precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] - move_to_table = Action(expr('MoveToTable(b, x)'), precond, effect) + move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) p = PlanningProblem(init, [move, move_to_table], goals) - n = astar_search(p) - print_solution(n) + for s in astar_search(p): + print_solution(s) def sussman_anomaly(): @@ -288,23 +318,63 @@ def sussman_anomaly(): # Move(b, x, y) precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] - move = Action(expr('Move(b, x, y)'), precond, effect) + move = PlanningAction(expr('Move(b, x, y)'), precond, effect) # MoveToTable(b, x) precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] - move_to_table = Action(expr('MoveToTable(b, x)'), precond, effect) + move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) p = PlanningProblem(init, [move, move_to_table], goals) - n = astar_search(p) - print_solution(n) + for s in astar_search(p): + print_solution(s) -if __name__ == '__main__': +def put_on_shoes(): + goals = [expr('On(RightShoe, RF)'), expr('On(LeftShoe, LF)')] + init = PlanningKB(goals, [expr('Clear(LF)'), + expr('Clear(RF)'), + expr('LeftFoot(LF)'), + expr('RightFoot(RF)')]) + + # Actions + # RightShoe + precond = [expr('On(RightSock, x)'), expr('RightFoot(x)'), expr('~On(RightShoe, x)')] + effect = [expr('On(RightShoe, x)')] + right_shoe = PlanningAction(expr('RightShoeOn'), precond, effect) + + # RightSock + precond = [expr('Clear(x)'), expr('RightFoot(x)')] + effect = [expr('On(RightSock, x)'), expr('~Clear(x)')] + right_sock = PlanningAction(expr('RightSockOn'), precond, effect) + + # LeftShoe + precond = [expr('On(LeftSock, x)'), expr('LeftFoot(x)'), expr('~On(LeftShoe, x)')] + effect = [expr('On(LeftShoe, x)')] + left_shoe = PlanningAction(expr('LeftShoeOn'), precond, effect) + + # LeftSock + precond = [expr('Clear(x)'), expr('LeftFoot(x)')] + effect = [expr('On(LeftSock, x)'), expr('~Clear(x)')] + left_sock = PlanningAction(expr('LeftSockOn'), precond, effect) + + p = PlanningProblem(init, [right_shoe, right_sock, left_shoe, left_sock], goals) + # find all six solutions as listed in the book + for s in depth_first_tree_search(p): + print_solution(s) + + +def tester(): + print('Air cargo solution:') air_cargo() - print() + print('\nSpare tire solution:') spare_tire() - print() + print('\nThree block tower solution:') three_block_tower() - print() + print('\nSussman anomaly solution:') sussman_anomaly() + + +if __name__ == '__main__': + import timeit + print(timeit.timeit("tester()", setup="from __main__ import tester", number=10)) \ No newline at end of file diff --git a/search.py b/search.py index 12a723662..b4824d9d5 100644 --- a/search.py +++ b/search.py @@ -181,7 +181,7 @@ def tree_search(problem, frontier): while frontier: node = frontier.pop() if problem.goal_test(node.state): - return node + yield node frontier.extend(node.expand(problem)) return None @@ -195,7 +195,7 @@ def graph_search(problem, frontier): while frontier: node = frontier.pop() if problem.goal_test(node.state): - return node + yield node explored.add(node.state) frontier.extend(child for child in node.expand(problem) if child.state not in explored and @@ -222,7 +222,7 @@ def breadth_first_search(problem): "[Figure 3.11]" node = Node(problem.initial) if problem.goal_test(node.state): - return node + yield node frontier = FIFOQueue() frontier.append(node) explored = set() @@ -232,7 +232,7 @@ def breadth_first_search(problem): for child in node.expand(problem): if child.state not in explored and child not in frontier: if problem.goal_test(child.state): - return child + yield child frontier.append(child) return None @@ -247,15 +247,24 @@ def best_first_graph_search(problem, f): a best first search you can examine the f values of the path returned.""" f = memoize(f, 'f') node = Node(problem.initial) + best_cost = sys.maxsize if problem.goal_test(node.state): - return node + if node.path_cost < best_cost: + best_cost = node.path_cost + yield node + else: + return None frontier = PriorityQueue(min, f) frontier.append(node) explored = set() while frontier: node = frontier.pop() if problem.goal_test(node.state): - return node + if node.path_cost < best_cost: + best_cost = node.path_cost + yield node + else: + return None explored.add(node.state) for child in node.expand(problem): if child.state not in explored and child not in frontier: diff --git a/utils.py b/utils.py index 16c94fb15..ee7b877d3 100644 --- a/utils.py +++ b/utils.py @@ -364,7 +364,6 @@ class Expr(object): op is a str like '+' or 'sin'; args are Expressions. Expr('x') or Symbol('x') creates a symbol (a nullary Expr). Expr('-', x) creates a unary; Expr('+', x, 1) creates a binary.""" - def __init__(self, op, *args): self.op = str(op) self.args = args From f1155ac4ecaaebc25ba655037f216e351685f822 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Fri, 13 Apr 2018 22:47:19 -0400 Subject: [PATCH 06/40] Initial add --- parse.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 parse.py diff --git a/parse.py b/parse.py new file mode 100644 index 000000000..22beadbbb --- /dev/null +++ b/parse.py @@ -0,0 +1,52 @@ +Symbol = str # A Lisp Symbol is implemented as a Python str +List = list # A Lisp List is implemented as a Python list + + +class ParseError(Exception): + pass + + +def read_pddl_file(filename) -> list: + with open(filename) 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 + s = ''.join(lines) + # transform into Python-compatible S-expressions (using lists of strings) + return parse(s) + + +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(pddl): + """Read PDDL contained in a string.""" + return read_from_tokens(tokenize(pddl)) + + +def tokenize(s): + """Convert a string into a list of tokens.""" + return s.replace('(', ' ( ').replace(')', ' ) ').split() + + +def read_from_tokens(tokens): + """Read an expression from a sequence of tokens.""" + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.pop(0) + if '(' == token: + L = [] + while tokens[0] != ')': + L.append(read_from_tokens(tokens)) + tokens.pop(0) # pop off ')' + return L + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return token From 8617fc0a8878f265b3decc61562e370abf553899 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 14 Apr 2018 22:23:15 -0400 Subject: [PATCH 07/40] Changed read_from_tokens to reverse lists by default, so that .pop() method can be used. Changed tokenize() to add whitespace around colons, so that actions parse correctly. --- parse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/parse.py b/parse.py index 22beadbbb..536ef2b5e 100644 --- a/parse.py +++ b/parse.py @@ -32,7 +32,7 @@ def parse(pddl): def tokenize(s): """Convert a string into a list of tokens.""" - return s.replace('(', ' ( ').replace(')', ' ) ').split() + return s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' : ').split() def read_from_tokens(tokens): @@ -45,6 +45,8 @@ def read_from_tokens(tokens): while tokens[0] != ')': L.append(read_from_tokens(tokens)) tokens.pop(0) # pop off ')' + # reverse each list so we can continue to use .pop() on it, and the elements will be in order. + L.reverse() return L elif ')' == token: raise SyntaxError('unexpected )') From a7fef4b15a9d946d12f34201b31a7d35ddf1dc13 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 14 Apr 2018 22:24:13 -0400 Subject: [PATCH 08/40] In the process of adding PDDL file parsing. --- planning.py | 334 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 259 insertions(+), 75 deletions(-) diff --git a/planning.py b/planning.py index 5a44a9fc6..0d1f4072b 100644 --- a/planning.py +++ b/planning.py @@ -1,10 +1,10 @@ """Planning (Chapters 10-11) """ -import copy -from logic import fol_bc_and, FolKB +from logic import fol_bc_and from utils import expr, Expr, partition -from search import Problem, astar_search, depth_first_tree_search, depth_first_graph_search -import timeit +from search import astar_search +from parse import read_pddl_file, ParseError +from collections.abc import MutableSequence class PlanningKB: @@ -52,21 +52,21 @@ def fetch_rules_for_goal(self, goal): return self.clause_set -class PlanningProblem(Problem): +class PlanningProblem: """ Used to define a planning problem. It stores states in a knowledge base consisting of first order logic statements. The conjunction of these logical statements completely define a state. """ - def __init__(self, initial_state, actions, goals): - super().__init__(initial_state, goals) - self.action_list = actions + def __init__(self, initial_kb, actions): + self.initial = initial_kb + self.possible_actions = actions def __repr__(self): - return '{}({}, {}, {})'.format(self.__class__.__name__, self.initial, self.action_list, self.goal) + return '{}({}, {})'.format(self.__class__.__name__, self.initial, self.possible_actions) def actions(self, state): - for action in self.action_list: + for action in self.possible_actions: for subst in action.check_precond(state): new_action = action.copy() new_action.subst = subst @@ -81,12 +81,24 @@ def result(self, state, action): 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 PlanningAction: """ Defines an action schema using preconditions and effects @@ -99,36 +111,26 @@ class PlanningAction: eat = Action(expr("Eat(person, food)"), precond, effect) """ - def __init__(self, expression, precond, effect): + def __init__(self, expression, preconds, effects): + self.expression = expression self.name = expression.op self.args = expression.args self.subst = None - - def is_negative_clause(e): - return e.op == '~' and len(e.args) == 1 - - precond_neg, precond_pos = partition(precond, 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 - effect_rem, effect_add = partition(effect, 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 + self.preconds = preconds + self.effects = effects def __repr__(self): - return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, self.args), - list(self.precond_pos) + ['~{0}'.format(p) for p in self.precond_neg], - list(self.effect_add) + ['~{0}'.format(e) for e in self.effect_rem]) + return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, *self.args), + list(self.preconds), list(self.effects)) def copy(self): - """Returns a copy of this object.""" - act = self.__new__(self.__class__, object) + """ 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() + act.preconds = self.preconds.copy() + act.effects = self.effects.copy() return act def substitute(self, subst, e): @@ -159,35 +161,222 @@ def check_pos_precond(self, kb, precond, subst): def check_precond(self, kb): """Checks if preconditions are satisfied in the current state""" - yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) + precond_neg, precond_pos = partition(self.preconds, is_negative_clause) + precond_neg = set(e.args[0] for e in precond_neg) # change the negative Exprs to positive + yield from self.check_neg_precond(kb, precond_neg, self.check_pos_precond(kb, precond_pos, {})) 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) + neg_effects, pos_effects = partition(self.effects, is_negative_clause) + neg_effects = set(self.substitute(subst, e.args[0]) for e in neg_effects) + pos_effects = set(self.substitute(subst, e) for e in pos_effects) + new_kb.clause_set = frozenset(kb.clause_set - neg_effects | pos_effects) return new_kb -class POPPlan: - def __init__(self, initial_kb): - precond = [] - effect = initial_kb.clause_set - start = PlanningAction(expr('Start'), precond, effect) - - precond = initial_kb.goal_clauses - effect = [] - finish = PlanningAction(expr('Finish'), precond, effect) - - self.action_list = [start, finish] - self.constraints = {(start, finish)} - self.causal_links = set() - self.open_preconds = set(initial_kb.goal_clauses) +class PDDLDomainParser: + def __init__(self): + self.domain_name = '' + self.action_name = '' + self.tokens = [] + self.requirements = [] + self.predicates = [] + self.actions = [] + self.types = [] + self.constants = [] + self.parameters = [] + self.preconditions = [] + self.effects = [] + + def _parse_define(self, tokens) -> bool: + domain_list = tokens.pop() + token = domain_list.pop() + if token != 'domain': + raise ParseError('domain keyword not found after define statement') + self.domain_name = domain_list.pop() + return True + + def _parse_requirements(self, tokens) -> bool: + self.requirements = tokens + if ':strips' not in self.requirements: + raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') + return True + + def _parse_constants(self, tokens) -> bool: + self.constants = self._parse_variables(tokens) + for const, ctype in self.constants: + if ctype not in self.types: + raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) + return True + + def _parse_types(self, tokens) -> bool: + self.types = tokens + return True + + def _parse_predicates(self, tokens) -> bool: + while tokens: + predicate = tokens.pop() + predicate.reverse() + new_predicate = [predicate[0]] + self._parse_variables(predicate) + self.predicates.append(new_predicate) + return True + + def _parse_variables(self, tokens) -> list: + variables = [] + num_tokens = len(tokens) + idx = 1 + while idx < num_tokens: + if not tokens[idx].startswith('?'): + raise ParseError("Unrecognized variable name ({0}) " + + "that doesn't begin with a question mark".format(tokens[idx])) + pred_var = tokens[idx][1:] + if not self.types: + variables.append(pred_var) + idx += 1 + else: + # lookahead to see if there's a dash indicating an upcoming type name + if tokens[idx + 1] == '-': + pred_type = tokens[idx + 2].lower() + if pred_type not in self.types: + raise ParseError("Predicate type {0} not in type list.".format(pred_type)) + else: + pred_type = None + arg = [pred_var, pred_type] + variables.append(arg) + # if any immediately prior variables didn't have an assigned type, then assign them this one. + for j in range(len(variables) - 1, 0, -1): + if variables[j][1] is not None: + break + else: + variables[j][1] = pred_type + idx += 3 + return variables + + def _parse_action(self, tokens) -> bool: + self.action_name = self.tokens[idx].lower() + idx += 1 + match = {':parameters': self._parse_parameters, + ':precondition': self._parse_precondition, + ':effect': self._parse_effect + } + idx = self.match_and_parse_tokens(idx, match) + return True + + def _parse_parameters(self, tokens) -> bool: + idx += 1 + if self.tokens[idx] != '(': + raise IOError('Start of parameter list is missing an open parenthesis.') + self.parameters.clear() + while idx < self.num_tokens: + if self.tokens[idx] == ')': + self.num_parens -= 1 + break + elif self.tokens[idx] == '(': + self.num_parens += 1 + try: + param_vars, idx = self._parse_variables(idx+1) + except IOError: + raise IOError('Action name {0} has an invalid argument list.'.format(self.action_name)) + self.parameters.extend(param_vars) + return True + + def _parse_single_expr(self, idx): + if self.tokens[idx+1] == 'not': + e = self._parse_single_expr(idx + 2) + if '~' in e: + raise IOError('Multiple not operators in expression.') + return expr('~' + e) + else: + if self.tokens[idx] != '(': + raise IOError('Expression in {0} is missing an open parenthesis.'.format(self.action_name)) + while idx < self.num_tokens: + if self.tokens[idx] == ')': + self.num_parens -= 1 + idx += 1 + break + elif self.tokens[idx] == '(': + expr_name = self.tokens[idx + 1] + variables = [] + idx += 2 + while idx < self.num_tokens: + if self.tokens[idx] == ')': + self.num_parens -= 1 + break + param = self.tokens[idx] + if param.startswith('?'): + variables.append(param.lower()) + else: + variables.append(param) + estr = expr_name + '(' + vlen = len(variables) + for i in range(vlen - 1): + estr += variables[i] + ', ' + estr += variables[vlen-1] + ')' + return estr + + def _parse_expr_list(self, idx): + expr_lst = [] + while idx < self.num_tokens: + if self.tokens[idx] == ')': + self.num_parens -= 1 + break + elif self.tokens[idx] == '(': + idx, expr = self._parse_single_expr(idx) + expr_lst.append(expr) + idx += 1 + return expr_lst + + def _parse_formula(self, idx, label): + expr_lst = [] + idx += 1 + if self.tokens[idx] == '(': + self.num_parens += 1 + else: + raise IOError('Start of {0} {1} is missing an open parenthesis.'.format(self.action_name, label)) + if self.tokens[idx + 1] == 'and': # preconds and effects only use 'and' keyword + exprs = self._parse_expr_list(idx + 2) + expr_lst.extend(exprs) + else: # parse single expression + expr = self._parse_single_expr(idx + 2) + expr_lst.append(expr) + return expr_lst + + def _parse_precondition(self, tokens): + idx, self.preconditions = self._parse_formula(idx, 'preconditions') + return True + + def _parse_effect(self, tokens): + idx, self.effects = self._parse_formula(idx, 'effects') + return True + + def read(self, filename): + pddl_list = read_pddl_file(filename) + + # 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 + } + + def parse_tokens(tokens): + if not tokens: + return + item = tokens.pop() + if isinstance(item, MutableSequence): + parse_tokens(item) + else: + for text in match: + if item.startswith(text): + if match[text](tokens): + break - def successors(self): - pass + while True: + parse_tokens(pddl_list) def print_solution(node): @@ -234,43 +423,35 @@ def air_cargo(): effect = [expr('At(p, to)'), expr('~At(p, f)')] fly = PlanningAction(expr('Fly(p, f, to)'), precond, effect) - p = PlanningProblem(init, [load, unload, fly], goals) - for solution in astar_search(p): - print_solution(solution) + p = PlanningProblem(init, [load, unload, fly]) + print_solution(astar_search(p)) def spare_tire(): goals = [expr('At(Spare, Axle)')] - init = PlanningKB(goals, [expr('At(Flat, Axle)'), expr('At(Spare, Trunk)')]) - # Actions # Remove(Spare, Trunk) precond = [expr('At(Spare, Trunk)')] effect = [expr('At(Spare, Ground)'), expr('~At(Spare, Trunk)')] remove_spare = PlanningAction(expr('Remove(Spare, Trunk)'), precond, effect) - # Remove(Flat, Axle) precond = [expr('At(Flat, Axle)')] effect = [expr('At(Flat, Ground)'), expr('~At(Flat, Axle)')] remove_flat = PlanningAction(expr('Remove(Flat, Axle)'), precond, effect) - # PutOn(Spare, Axle) precond = [expr('At(Spare, Ground)'), expr('~At(Flat, Axle)')] effect = [expr('At(Spare, Axle)'), expr('~At(Spare, Ground)')] put_on_spare = PlanningAction(expr('PutOn(Spare, Axle)'), precond, effect) - # LeaveOvernight precond = [] effect = [expr('~At(Spare, Ground)'), expr('~At(Spare, Axle)'), expr('~At(Spare, Trunk)'), expr('~At(Flat, Ground)'), expr('~At(Flat, Axle)')] leave_overnight = PlanningAction(expr('LeaveOvernight'), precond, effect) - - p = PlanningProblem(init, [remove_spare, remove_flat, put_on_spare, leave_overnight], goals) - for s in astar_search(p): - print_solution(s) + p = PlanningProblem(init, [remove_spare, remove_flat, put_on_spare, leave_overnight]) + print_solution(astar_search(p)) def three_block_tower(): @@ -297,9 +478,8 @@ def three_block_tower(): effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) - p = PlanningProblem(init, [move, move_to_table], goals) - for s in astar_search(p): - print_solution(s) + p = PlanningProblem(init, [move, move_to_table]) + print_solution(astar_search(p)) def sussman_anomaly(): @@ -325,9 +505,8 @@ def sussman_anomaly(): effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) - p = PlanningProblem(init, [move, move_to_table], goals) - for s in astar_search(p): - print_solution(s) + p = PlanningProblem(init, [move, move_to_table]) + print_solution(astar_search(p)) def put_on_shoes(): @@ -358,10 +537,13 @@ def put_on_shoes(): effect = [expr('On(LeftSock, x)'), expr('~Clear(x)')] left_sock = PlanningAction(expr('LeftSockOn'), precond, effect) - p = PlanningProblem(init, [right_shoe, right_sock, left_shoe, left_sock], goals) - # find all six solutions as listed in the book - for s in depth_first_tree_search(p): - print_solution(s) + p = PlanningProblem(init, [right_shoe, right_sock, left_shoe, left_sock]) + print_solution(astar_search(p)) + + +def parse_domain_file(filename): + parser = PDDLDomainParser() + parser.read(filename) def tester(): @@ -373,8 +555,10 @@ def tester(): three_block_tower() print('\nSussman anomaly solution:') sussman_anomaly() + print('\nPut on shoes solution:') + put_on_shoes() + parse_domain_file('blocks-domain.pddl') if __name__ == '__main__': - import timeit - print(timeit.timeit("tester()", setup="from __main__ import tester", number=10)) \ No newline at end of file + tester() From c4b04ce3dd4f3154c8291b7f319305e895df5352 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 14 Apr 2018 22:25:50 -0400 Subject: [PATCH 09/40] Reverted back to current AIMA state (removing yield statements). --- search.py | 571 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 433 insertions(+), 138 deletions(-) mode change 100644 => 100755 search.py diff --git a/search.py b/search.py old mode 100644 new mode 100755 index b4824d9d5..ac834d80c --- a/search.py +++ b/search.py @@ -5,17 +5,18 @@ functions.""" from utils import ( - is_in, argmin, argmax, argmax_random_tie, probability, - weighted_sample_with_replacement, memoize, print_table, DataFile, Stack, - FIFOQueue, PriorityQueue, name + is_in, argmin, argmax, argmax_random_tie, probability, weighted_sampler, + memoize, print_table, open_data, Stack, FIFOQueue, PriorityQueue, name, + distance, vector_add ) -from grid import distance from collections import defaultdict import math import random import sys import bisect +from operator import itemgetter + infinity = float('inf') @@ -24,14 +25,14 @@ class Problem(object): - """The abstract class for a formal problem. You should subclass + """The abstract class for a formal problem. You should subclass this and implement the methods actions and result, and possibly __init__, goal_test, and path_cost. Then you will create instances of your subclass and solve them with the various search functions.""" def __init__(self, initial, goal=None): """The constructor specifies the initial state, and possibly a goal - state, if there is a unique goal. Your subclass's constructor can add + state, if there is a unique goal. Your subclass's constructor can add other arguments.""" self.initial = initial self.goal = goal @@ -86,7 +87,7 @@ class Node: subclass this class.""" def __init__(self, state, parent=None, action=None, path_cost=0): - "Create a search tree Node, derived from a parent by an action." + """Create a search tree Node, derived from a parent by an action.""" self.state = state self.parent = parent self.action = action @@ -96,29 +97,29 @@ def __init__(self, state, parent=None, action=None, path_cost=0): self.depth = parent.depth + 1 def __repr__(self): - return "" % (self.state,) + return "".format(self.state) def __lt__(self, node): return self.state < node.state def expand(self, problem): - "List the nodes reachable in one step from this node." + """List the nodes reachable in one step from this node.""" return [self.child_node(problem, action) for action in problem.actions(self.state)] def child_node(self, problem, action): - "[Figure 3.10]" + """[Figure 3.10]""" next = problem.result(self.state, action) return Node(next, self, action, problem.path_cost(self.path_cost, self.state, action, next)) def solution(self): - "Return the sequence of actions to go from the root to this node." + """Return the sequence of actions to go from the root to this node.""" return [node.action for node in self.path()[1:]] def path(self): - "Return a list of nodes forming the path from the root to this node." + """Return a list of nodes forming the path from the root to this node.""" node, path_back = self, [] while node: path_back.append(node) @@ -144,10 +145,15 @@ class SimpleProblemSolvingAgentProgram: """Abstract framework for a problem-solving agent. [Figure 3.1]""" def __init__(self, initial_state=None): + """State is an abstract representation of the state + of the world, and seq is the list of actions required + to get to a particular state from the initial state(root).""" self.state = initial_state self.seq = [] def __call__(self, percept): + """[Figure 3.1] Formulate a goal and problem, then + search for a sequence of actions to solve it.""" self.state = self.update_state(self.state, percept) if not self.seq: goal = self.formulate_goal(self.state) @@ -181,7 +187,7 @@ def tree_search(problem, frontier): while frontier: node = frontier.pop() if problem.goal_test(node.state): - yield node + return node frontier.extend(node.expand(problem)) return None @@ -195,7 +201,7 @@ def graph_search(problem, frontier): while frontier: node = frontier.pop() if problem.goal_test(node.state): - yield node + return node explored.add(node.state) frontier.extend(child for child in node.expand(problem) if child.state not in explored and @@ -204,25 +210,25 @@ def graph_search(problem, frontier): def breadth_first_tree_search(problem): - "Search the shallowest nodes in the search tree first." + """Search the shallowest nodes in the search tree first.""" return tree_search(problem, FIFOQueue()) def depth_first_tree_search(problem): - "Search the deepest nodes in the search tree first." + """Search the deepest nodes in the search tree first.""" return tree_search(problem, Stack()) def depth_first_graph_search(problem): - "Search the deepest nodes in the search tree first." + """Search the deepest nodes in the search tree first.""" return graph_search(problem, Stack()) def breadth_first_search(problem): - "[Figure 3.11]" + """[Figure 3.11]""" node = Node(problem.initial) if problem.goal_test(node.state): - yield node + return node frontier = FIFOQueue() frontier.append(node) explored = set() @@ -232,7 +238,7 @@ def breadth_first_search(problem): for child in node.expand(problem): if child.state not in explored and child not in frontier: if problem.goal_test(child.state): - yield child + return child frontier.append(child) return None @@ -247,24 +253,15 @@ def best_first_graph_search(problem, f): a best first search you can examine the f values of the path returned.""" f = memoize(f, 'f') node = Node(problem.initial) - best_cost = sys.maxsize if problem.goal_test(node.state): - if node.path_cost < best_cost: - best_cost = node.path_cost - yield node - else: - return None + return node frontier = PriorityQueue(min, f) frontier.append(node) explored = set() while frontier: node = frontier.pop() if problem.goal_test(node.state): - if node.path_cost < best_cost: - best_cost = node.path_cost - yield node - else: - return None + return node explored.add(node.state) for child in node.expand(problem): if child.state not in explored and child not in frontier: @@ -278,12 +275,12 @@ def best_first_graph_search(problem, f): def uniform_cost_search(problem): - "[Figure 3.14]" + """[Figure 3.14]""" return best_first_graph_search(problem, lambda node: node.path_cost) def depth_limited_search(problem, limit=50): - "[Figure 3.17]" + """[Figure 3.17]""" def recursive_dls(node, problem, limit): if problem.goal_test(node.state): return node @@ -304,15 +301,95 @@ def recursive_dls(node, problem, limit): def iterative_deepening_search(problem): - "[Figure 3.18]" + """[Figure 3.18]""" for depth in range(sys.maxsize): result = depth_limited_search(problem, depth) if result != 'cutoff': return result +# ______________________________________________________________________________ +# Bidirectional Search +# Pseudocode from https://webdocs.cs.ualberta.ca/%7Eholte/Publications/MM-AAAI2016.pdf + +def bidirectional_search(problem): + e = problem.find_min_edge() + gF, gB = {problem.initial : 0}, {problem.goal : 0} + openF, openB = [problem.initial], [problem.goal] + closedF, closedB = [], [] + U = infinity + + + def extend(U, open_dir, open_other, g_dir, g_other, closed_dir): + """Extend search in given direction""" + n = find_key(C, open_dir, g_dir) + + open_dir.remove(n) + closed_dir.append(n) + + for c in problem.actions(n): + if c in open_dir or c in closed_dir: + if g_dir[c] <= problem.path_cost(g_dir[n], n, None, c): + continue + + open_dir.remove(c) + + g_dir[c] = problem.path_cost(g_dir[n], n, None, c) + open_dir.append(c) + + if c in open_other: + U = min(U, g_dir[c] + g_other[c]) + + return U, open_dir, closed_dir, g_dir + + + def find_min(open_dir, g): + """Finds minimum priority, g and f values in open_dir""" + m, m_f = infinity, infinity + for n in open_dir: + f = g[n] + problem.h(n) + pr = max(f, 2*g[n]) + m = min(m, pr) + m_f = min(m_f, f) + + return m, m_f, min(g.values()) + + + def find_key(pr_min, open_dir, g): + """Finds key in open_dir with value equal to pr_min + and minimum g value.""" + m = infinity + state = -1 + for n in open_dir: + pr = max(g[n] + problem.h(n), 2*g[n]) + if pr == pr_min: + if g[n] < m: + m = g[n] + state = n + + return state + + + while openF and openB: + pr_min_f, f_min_f, g_min_f = find_min(openF, gF) + pr_min_b, f_min_b, g_min_b = find_min(openB, gB) + C = min(pr_min_f, pr_min_b) + + if U <= max(C, f_min_f, f_min_b, g_min_f + g_min_b + e): + return U + + if C == pr_min_f: + # Extend forward + U, openF, closedF, gF = extend(U, openF, openB, gF, gB, closedF) + else: + # Extend backward + U, openB, closedB, gB = extend(U, openB, openF, gB, gF, closedB) + + return infinity + # ______________________________________________________________________________ # Informed (Heuristic) Search + greedy_best_first_graph_search = best_first_graph_search # Greedy best-first search is accomplished by specifying f(n) = h(n). @@ -324,12 +401,114 @@ def astar_search(problem, h=None): h = memoize(h or problem.h, 'h') return best_first_graph_search(problem, lambda n: n.path_cost + h(n)) +# ______________________________________________________________________________ +# 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=None): + if goal: + self.goal = goal + else: + self.goal = [ [0,1,2], + [3,4,5], + [6,7,8] ] + Problem.__init__(self, initial, goal) + + def find_blank_square(self, state): + """Return the index of the blank square in a given state""" + for row in len(state): + for column in len(row): + if state[row][column] == 0: + index_blank_square = (row, column) + return index_blank_square + + 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 = list() + index_blank_square = self.find_blank_square(state) + + if index_blank_square(0) == 0: + possible_actions += ['DOWN'] + elif index_blank_square(0) == 1: + possible_actions += ['UP', 'DOWN'] + elif index_blank_square(0) == 2: + possible_actions += ['UP'] + + if index_blank_square(1) == 0: + possible_actions += ['RIGHT'] + elif index_blank_square(1) == 1: + possible_actions += ['LEFT', 'RIGHT'] + elif index_blank_square(1) == 2: + possible_actions += ['LEFT'] + + return possible_actions + + def result(self, state, action): + """Given state and action, return a new state that is the result of the action. + Action is assumed to be a valid action in the state.""" + + blank_square = self.find_blank_square(state) + new_state = [row[:] for row in state] + + if action=='UP': + new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)-1][blank_square(1)] + new_state[blank_square(0)-1][blank_square(1)] = 0 + elif action=='LEFT': + new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)][blank_square(1)-1] + new_state[blank_square(0)][blank_square(1)-1] = 0 + elif action=='DOWN': + new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)+1][blank_square(1)] + new_state[blank_square(0)+1][blank_square(1)] = 0 + elif action=='RIGHT': + new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)][blank_square(1)+1] + new_state[blank_square(0)][blank_square(1)+1] = 0 + else: + print("Invalid Action!") + return new_state + + def goal_test(self, state): + """Given a state, return True if state is a goal state or False, otherwise""" + for row in len(state): + for column in len(row): + if state[row][col] != self.goal[row][column]: + return False + return True + + def checkSolvability(self, state): + inversion = 0 + for i in range(len(state)): + for j in range(i, len(state)): + if (state[i] > state[j] and state[j] != 0): + inversion += 1 + check = True + if inversion%2 != 0: + check = False + print(check) + + def h(self, state): + """Return the heuristic value for a given state. Heuristic function used is + h(n) = number of misplaced tiles.""" + num_misplaced_tiles = 0 + for row in len(state): + for column in len(row): + if state[row][col] != self.goal[row][column]: + num_misplaced_tiles += 1 + return num_misplaced_tiles + # ______________________________________________________________________________ # Other search algorithms def recursive_best_first_search(problem, h=None): - "[Figure 3.26]" + """[Figure 3.26]""" h = memoize(h or problem.h, 'h') def RBFS(problem, node, flimit): @@ -377,38 +556,57 @@ def hill_climbing(problem): def exp_schedule(k=20, lam=0.005, limit=100): - "One possible schedule function for simulated annealing" + """One possible schedule function for simulated annealing""" return lambda t: (k * math.exp(-lam * t) if t < limit else 0) def simulated_annealing(problem, schedule=exp_schedule()): - "[Figure 4.5]" + """[Figure 4.5] CAUTION: This differs from the pseudocode as it + returns a state instead of a Node.""" current = Node(problem.initial) for t in range(sys.maxsize): T = schedule(t) if T == 0: - return current + return current.state neighbors = current.expand(problem) if not neighbors: - return current + return current.state next = random.choice(neighbors) delta_e = problem.value(next.state) - problem.value(current.state) if delta_e > 0 or probability(math.exp(delta_e / T)): current = next +def simulated_annealing_full(problem, schedule=exp_schedule()): + """ This version returns all the states encountered in reaching + the goal state.""" + states = [] + current = Node(problem.initial) + for t in range(sys.maxsize): + states.append(current.state) + T = schedule(t) + if T == 0: + return states + neighbors = current.expand(problem) + if not neighbors: + return current.state + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) + if delta_e > 0 or probability(math.exp(delta_e / T)): + current = next def and_or_graph_search(problem): - """Used when the environment is nondeterministic and completely observable - Contains OR nodes where the agent is free to choose any action + """[Figure 4.11]Used when the environment is nondeterministic and completely observable. + Contains OR nodes where the agent is free to choose any action. After every action there is an AND node which contains all possible states - the agent may reach due to stochastic nature of environment - The agent must be able to handle all possible states of the AND node(as it - may end up in any of them) returns a conditional plan to reach goal state, - or failure if the former is not possible""" - "[Figure 4.11]" + the agent may reach due to stochastic nature of environment. + The agent must be able to handle all possible states of the AND node (as it + may end up in any of them). + Returns a conditional plan to reach goal state, + or failure if the former is not possible.""" # functions used by and_or_search def or_search(state, problem, path): + """returns a plan as a list of actions""" if problem.goal_test(state): return [] if state in path: @@ -420,7 +618,7 @@ def or_search(state, problem, path): return [action, plan] def and_search(states, problem, path): - "returns plan in form of dictionary where we take action plan[s] if we reach state s" # noqa + """Returns plan in form of dictionary where we take action plan[s] if we reach state s.""" plan = {} for s in states: plan[s] = or_search(s, problem, path) @@ -431,13 +629,52 @@ def and_search(states, problem, path): # body of and or search return or_search(problem.initial, problem, []) +# Pre-defined actions for PeakFindingProblem +directions4 = { 'W':(-1, 0), 'N':(0, 1), 'E':(1, 0), 'S':(0, -1) } +directions8 = dict(directions4) +directions8.update({'NW':(-1, 1), 'NE':(1, 1), 'SE':(1, -1), 'SW':(-1, -1) }) + +class PeakFindingProblem(Problem): + """Problem of finding the highest peak in a limited grid""" + + def __init__(self, initial, grid, defined_actions=directions4): + """The grid is a 2 dimensional array/list whose state is specified by tuple of indices""" + Problem.__init__(self, initial) + self.grid = grid + self.defined_actions = defined_actions + self.n = len(grid) + assert self.n > 0 + self.m = len(grid[0]) + assert self.m > 0 + + def actions(self, state): + """Returns the list of actions which are allowed to be taken from the given state""" + allowed_actions = [] + for action in self.defined_actions: + next_state = vector_add(state, self.defined_actions[action]) + if next_state[0] >= 0 and next_state[1] >= 0 and next_state[0] <= self.n - 1 and next_state[1] <= self.m - 1: + allowed_actions.append(action) + + return allowed_actions + + def result(self, state, action): + """Moves in the direction specified by action""" + return vector_add(state, self.defined_actions[action]) + + def value(self, state): + """Value of a state is the value it is the index to""" + x, y = state + assert 0 <= x < self.n + assert 0 <= y < self.m + return self.grid[x][y] + class OnlineDFSAgent: - """The abstract class for an OnlineDFSAgent. Override update_state - method to convert percept to state. While initializing the subclass - a problem needs to be provided which is an instance of a subclass - of the Problem class. [Figure 4.21] """ + """[Figure 4.21] The abstract class for an OnlineDFSAgent. Override + update_state method to convert percept to state. While initializing + the subclass a problem needs to be provided which is an instance of + a subclass of the Problem class.""" def __init__(self, problem): self.problem = problem @@ -457,13 +694,13 @@ def __call__(self, percept): if self.s is not None: if s1 != self.result[(self.s, self.a)]: self.result[(self.s, self.a)] = s1 - unbacktracked[s1].insert(0, self.s) + self.unbacktracked[s1].insert(0, self.s) if len(self.untried[s1]) == 0: if len(self.unbacktracked[s1]) == 0: self.a = None else: - # else a <- an action b such that result[s', b] = POP(unbacktracked[s']) # noqa - unbacktracked_pop = self.unbacktracked[s1].pop(0) # noqa + # else a <- an action b such that result[s', b] = POP(unbacktracked[s']) + unbacktracked_pop = self.unbacktracked[s1].pop(0) for (s, b) in self.result.keys(): if self.result[(s, b)] == unbacktracked_pop: self.a = b @@ -474,8 +711,8 @@ def __call__(self, percept): return self.a def update_state(self, percept): - '''To be overridden in most cases. The default case - assumes the percept to be of type state.''' + """To be overridden in most cases. The default case + assumes the percept to be of type state.""" return percept # ______________________________________________________________________________ @@ -485,8 +722,8 @@ class OnlineSearchProblem(Problem): """ A problem which is solved by an agent executing actions, rather than by just computation. - Carried in a deterministic and a fully observable environment. - """ + Carried in a deterministic and a fully observable environment.""" + def __init__(self, initial, goal, graph): self.initial = initial self.goal = goal @@ -499,15 +736,11 @@ def output(self, state, action): return self.graph.dict[state][action] def h(self, state): - """ - Returns least possible cost to reach a goal for the given state. - """ + """Returns least possible cost to reach a goal for the given state.""" return self.graph.least_costs[state] def c(self, s, a, s1): - """ - Returns a cost estimate for an agent to move from state 's' to state 's1' - """ + """Returns a cost estimate for an agent to move from state 's' to state 's1'.""" return 1 def update_state(self, percept): @@ -523,9 +756,9 @@ class LRTAStarAgent: """ [Figure 4.24] Abstract class for LRTA*-Agent. A problem needs to be - provided which is an instanace of a subclass of Problem Class. + provided which is an instance of a subclass of Problem Class. - Takes a OnlineSearchProblem [Figure 4.23] as a problem + Takes a OnlineSearchProblem [Figure 4.23] as a problem. """ def __init__(self, problem): @@ -546,23 +779,19 @@ def __call__(self, s1): # as of now s1 is a state rather than a percept # self.result[(self.s, self.a)] = s1 # no need as we are using problem.output # minimum cost for action b in problem.actions(s) - self.H[self.s] = min(self.LRTA_cost(self.s, b, self.problem.output(self.s, b), self.H) - for b in self.problem.actions(self.s)) + self.H[self.s] = min(self.LRTA_cost(self.s, b, self.problem.output(self.s, b), + self.H) for b in self.problem.actions(self.s)) - # costs for action b in problem.actions(s1) - costs = [self.LRTA_cost(s1, b, self.problem.output(s1, b), self.H) - for b in self.problem.actions(s1)] # an action b in problem.actions(s1) that minimizes costs - self.a = list(self.problem.actions(s1))[costs.index(min(costs))] + self.a = argmin(self.problem.actions(s1), + key=lambda b: self.LRTA_cost(s1, b, self.problem.output(s1, b), self.H)) self.s = s1 return self.a def LRTA_cost(self, s, a, s1, H): - """ - Returns cost to move from state 's' to state 's1' plus - estimated cost to get to goal from s1 - """ + """Returns cost to move from state 's' to state 's1' plus + estimated cost to get to goal from s1.""" print(s, a, s1) if s1 is None: return self.problem.h(s) @@ -579,46 +808,95 @@ def LRTA_cost(self, s, a, s1, H): def genetic_search(problem, fitness_fn, ngen=1000, pmut=0.1, n=20): - """ - Call genetic_algorithm on the appropriate parts of a problem. + """Call genetic_algorithm on the appropriate parts of a problem. This requires the problem to have states that can mate and mutate, plus a value method that scores states.""" + + # NOTE: This is not tested and might not work. + # TODO: Use this function to make Problems work with genetic_algorithm. + s = problem.initial_state states = [problem.result(s, a) for a in problem.actions(s)] random.shuffle(states) return genetic_algorithm(states[:n], problem.value, ngen, pmut) -def genetic_algorithm(population, fitness_fn, ngen=1000, pmut=0.1): - "[Figure 4.8]" +def genetic_algorithm(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1000, pmut=0.1): + """[Figure 4.8]""" for i in range(ngen): - new_population = [] - for i in len(population): - fitnesses = map(fitness_fn, population) - p1, p2 = weighted_sample_with_replacement(population, fitnesses, 2) - child = p1.mate(p2) - if random.uniform(0, 1) < pmut: - child.mutate() - new_population.append(child) - population = new_population + population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut) + for i in range(len(population))] + + fittest_individual = fitness_threshold(fitness_fn, f_thres, population) + if fittest_individual: + return fittest_individual + + return argmax(population, key=fitness_fn) -class GAState: +def fitness_threshold(fitness_fn, f_thres, population): + if not f_thres: + return None + + fittest_individual = argmax(population, key=fitness_fn) + if fitness_fn(fittest_individual) >= f_thres: + return fittest_individual - "Abstract class for individuals in a genetic search." + return None - def __init__(self, genes): - self.genes = genes - def mate(self, other): - "Return a new individual crossing self and other." - c = random.randrange(len(self.genes)) - return self.__class__(self.genes[:c] + other.genes[c:]) - def mutate(self): - "Change a few of my genes." - raise NotImplementedError +def init_population(pop_number, gene_pool, state_length): + """Initializes population for genetic algorithm + pop_number : Number of individuals in population + gene_pool : List of possible values for individuals + state_length: The length of each individual""" + g = len(gene_pool) + population = [] + for i in range(pop_number): + new_individual = [gene_pool[random.randrange(0, g)] for j in range(state_length)] + population.append(new_individual) + + return population + + +def select(r, population, fitness_fn): + fitnesses = map(fitness_fn, population) + sampler = weighted_sampler(population, fitnesses) + return [sampler() for i in range(r)] + + +def recombine(x, y): + n = len(x) + c = random.randrange(0, n) + return x[:c] + y[c:] + + +def recombine_uniform(x, y): + n = len(x) + result = [0] * n; + indexes = random.sample(range(n), n) + for i in range(n): + ix = indexes[i] + result[ix] = x[ix] if i < n / 2 else y[ix] + try: + return ''.join(result) + except: + return result + + +def mutate(x, gene_pool, pmut): + if random.uniform(0, 1) >= pmut: + return x + + n = len(x) + g = len(gene_pool) + c = random.randrange(0, n) + r = random.randrange(0, g) + + new_gene = gene_pool[r] + return x[:c] + [new_gene] + x[c+1:] # _____________________________________________________________________________ # The remainder of this file implements examples for the search algorithms. @@ -629,7 +907,7 @@ def mutate(self): class Graph: - """A graph connects nodes (verticies) by edges (links). Each edge can also + """A graph connects nodes (vertices) by edges (links). Each edge can also have a length associated with it. The constructor call is something like: g = Graph({'A': {'B': 1, 'C': 2}) this makes a graph with 3 nodes, A, B, and C, with an edge of length 1 from @@ -649,10 +927,10 @@ def __init__(self, dict=None, directed=True): self.make_undirected() def make_undirected(self): - "Make a digraph into an undirected graph by adding symmetric edges." + """Make a digraph into an undirected graph by adding symmetric edges.""" for a in list(self.dict.keys()): - for (b, distance) in self.dict[a].items(): - self.connect1(b, a, distance) + for (b, dist) in self.dict[a].items(): + self.connect1(b, a, dist) def connect(self, A, B, distance=1): """Add a link from A and B of given distance, and also add the inverse @@ -662,7 +940,7 @@ def connect(self, A, B, distance=1): self.connect1(B, A, distance) def connect1(self, A, B, distance): - "Add a link from A to B of given distance, in one direction only." + """Add a link from A to B of given distance, in one direction only.""" self.dict.setdefault(A, {})[B] = distance def get(self, a, b=None): @@ -676,12 +954,12 @@ def get(self, a, b=None): return links.get(b) def nodes(self): - "Return a list of nodes in the graph." + """Return a list of nodes in the graph.""" return list(self.dict.keys()) def UndirectedGraph(dict=None): - "Build a Graph where every edge (including future ones) goes both ways." + """Build a Graph where every edge (including future ones) goes both ways.""" return Graph(dict=dict, directed=False) @@ -713,6 +991,7 @@ def distance_to_node(n): g.connect(node, neighbor, int(d)) return g + """ [Figure 3.2] Simplified road map of Romania """ @@ -742,7 +1021,8 @@ def distance_to_node(n): """ [Figure 4.9] Eight possible states of the vacumm world Each state is represented as - * "State of the left room" "State of the right room" "Room in which the agent is present" + * "State of the left room" "State of the right room" "Room in which the agent + is present" 1 - DDL Dirty Dirty Left 2 - DDR Dirty Dirty Right 3 - DCL Dirty Clean Left @@ -753,14 +1033,14 @@ def distance_to_node(n): 8 - CCR Clean Clean Right """ vacumm_world = Graph(dict( - State_1 = dict(Suck = ['State_7', 'State_5'], Right = ['State_2']), - State_2 = dict(Suck = ['State_8', 'State_4'], Left = ['State_2']), - State_3 = dict(Suck = ['State_7'], Right = ['State_4']), - State_4 = dict(Suck = ['State_4', 'State_2'], Left = ['State_3']), - State_5 = dict(Suck = ['State_5', 'State_1'], Right = ['State_6']), - State_6 = dict(Suck = ['State_8'], Left = ['State_5']), - State_7 = dict(Suck = ['State_7', 'State_3'], Right = ['State_8']), - State_8 = dict(Suck = ['State_8', 'State_6'], Left = ['State_7']) + State_1=dict(Suck=['State_7', 'State_5'], Right=['State_2']), + State_2=dict(Suck=['State_8', 'State_4'], Left=['State_2']), + State_3=dict(Suck=['State_7'], Right=['State_4']), + State_4=dict(Suck=['State_4', 'State_2'], Left=['State_3']), + State_5=dict(Suck=['State_5', 'State_1'], Right=['State_6']), + State_6=dict(Suck=['State_8'], Left=['State_5']), + State_7=dict(Suck=['State_7', 'State_3'], Right=['State_8']), + State_8=dict(Suck=['State_8', 'State_6'], Left=['State_7']) )) """ [Figure 4.23] @@ -797,27 +1077,39 @@ def distance_to_node(n): class GraphProblem(Problem): - "The problem of searching a graph from one node to another." + """The problem of searching a graph from one node to another.""" def __init__(self, initial, goal, graph): Problem.__init__(self, initial, goal) self.graph = graph def actions(self, A): - "The actions at a graph node are just its neighbors." + """The actions at a graph node are just its neighbors.""" return list(self.graph.get(A).keys()) def result(self, state, action): - "The result of going to a neighbor is just that neighbor." + """The result of going to a neighbor is just that neighbor.""" return action def path_cost(self, cost_so_far, A, action, B): return cost_so_far + (self.graph.get(A, B) or infinity) + def find_min_edge(self): + """Find minimum value of edges.""" + m = infinity + for d in self.graph.dict.values(): + local_min = min(d.values()) + m = min(m, local_min) + + return m + def h(self, node): - "h function is straight-line distance from a node's state to goal." + """h function is straight-line distance from a node's state to goal.""" locs = getattr(self.graph, 'locations', None) if locs: + if type(node) is str: + return int(distance(locs[node], locs[self.goal])) + return int(distance(locs[node.state], locs[self.goal])) else: return infinity @@ -826,16 +1118,16 @@ def h(self, node): class GraphProblemStochastic(GraphProblem): """ A version of GraphProblem where an action can lead to - nondeterministic output i.e. multiple possible states + nondeterministic output i.e. multiple possible states. Define the graph as dict(A = dict(Action = [[, , ...], ], ...), ...) - A the dictionary format is different, make sure the graph is created as a directed graph + A the dictionary format is different, make sure the graph is created as a directed graph. """ def result(self, state, action): return self.graph.get(state, action) - def path_cost(): + def path_cost(self): raise NotImplementedError @@ -858,7 +1150,7 @@ def __init__(self, N): self.initial = [None] * N def actions(self, state): - "In the leftmost empty column, try all non-conflicting rows." + """In the leftmost empty column, try all non-conflicting rows.""" if state[-1] is not None: return [] # All columns filled; no successors else: @@ -867,26 +1159,26 @@ def actions(self, state): if not self.conflicted(state, row, col)] def result(self, state, row): - "Place the next queen at the given row." + """Place the next queen at the given row.""" col = state.index(None) new = state[:] new[col] = row return new def conflicted(self, state, row, col): - "Would placing a queen at (row, col) conflict with anything?" + """Would placing a queen at (row, col) conflict with anything?""" return any(self.conflict(row, col, state[c], c) for c in range(col)) def conflict(self, row1, col1, row2, col2): - "Would putting two queens in (row1, col1) and (row2, col2) conflict?" + """Would putting two queens in (row1, col1) and (row2, col2) conflict?""" return (row1 == row2 or # same row col1 == col2 or # same column row1 - col1 == row2 - col2 or # same \ diagonal row1 + col1 == row2 + col2) # same / diagonal def goal_test(self, state): - "Check if all columns filled, no conflicts." + """Check if all columns filled, no conflicts.""" if state[-1] is None: return False return not any(self.conflicted(state, state[col], col) @@ -896,6 +1188,7 @@ def goal_test(self, state): # Inverse Boggle: Search for a high-scoring Boggle board. A good domain for # iterative-repair and related search techniques, as suggested by Justin Boyan. + ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' cubes16 = ['FORIXB', 'MOQABJ', 'GURILW', 'SETUPL', @@ -914,11 +1207,12 @@ def random_boggle(n=4): # The best 5x5 board found by Boyan, with our word list this board scores # 2274 words, for a score of 9837 + boyan_best = list('RSTCSDEIAEGNLRPEATESMSSID') def print_boggle(board): - "Print the board in a 2-d array." + """Print the board in a 2-d array.""" n2 = len(board) n = exact_sqrt(n2) for i in range(n2): @@ -966,7 +1260,7 @@ def boggle_neighbors(n2, cache={}): def exact_sqrt(n2): - "If n2 is a perfect square, return its square root, else raise error." + """If n2 is a perfect square, return its square root, else raise error.""" n = int(math.sqrt(n2)) assert n * n == n2 return n @@ -1015,19 +1309,19 @@ def __len__(self): class BoggleFinder: - """A class that allows you to find all the words in a Boggle board. """ + """A class that allows you to find all the words in a Boggle board.""" wordlist = None # A class variable, holding a wordlist def __init__(self, board=None): if BoggleFinder.wordlist is None: - BoggleFinder.wordlist = Wordlist(DataFile("EN-text/wordlist.txt")) + BoggleFinder.wordlist = Wordlist(open_data("EN-text/wordlist.txt")) self.found = {} if board: self.set_board(board) def set_board(self, board=None): - "Set the board, and find all the words in it." + """Set the board, and find all the words in it.""" if board is None: board = random_boggle() self.board = board @@ -1058,17 +1352,17 @@ def find(self, lo, hi, i, visited, prefix): visited.pop() def words(self): - "The words found." + """The words found.""" return list(self.found.keys()) scores = [0, 0, 0, 0, 1, 2, 3, 5] + [11] * 100 def score(self): - "The total score for the words found, according to the rules." + """The total score for the words found, according to the rules.""" return sum([self.scores[len(w)] for w in self.words()]) def __len__(self): - "The number of words found." + """The number of words found.""" return len(self.found) # _____________________________________________________________________________ @@ -1141,8 +1435,8 @@ def __getattr__(self, attr): return getattr(self.problem, attr) def __repr__(self): - return '<%4d/%4d/%4d/%s>' % (self.succs, self.goal_tests, - self.states, str(self.found)[:4]) + return '<{:4d}/{:4d}/{:4d}/{}>'.format(self.succs, self.goal_tests, + self.states, str(self.found)[:4]) def compare_searchers(problems, header, @@ -1167,3 +1461,4 @@ def compare_graph_searchers(): GraphProblem('Q', 'WA', australia_map)], header=['Searcher', 'romania_map(Arad, Bucharest)', 'romania_map(Oradea, Neamt)', 'australia_map']) + From c65ac28de4f3694471a415535656faba844b17cb Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 14 Apr 2018 22:35:12 -0400 Subject: [PATCH 10/40] Incorporated latest AIMA changes while leaving my partition function intact. --- utils.py | 349 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 265 insertions(+), 84 deletions(-) mode change 100644 => 100755 utils.py diff --git a/utils.py b/utils.py old mode 100644 new mode 100755 index ee7b877d3..537a7510d --- a/utils.py +++ b/utils.py @@ -3,11 +3,13 @@ import bisect import collections import collections.abc -import functools import operator import os.path import random import math +import functools +from itertools import chain, combinations + # ______________________________________________________________________________ # Functions on Sequences and Iterables @@ -32,7 +34,7 @@ def sequence(iterable): def removeall(item, seq): - """Return a copy of seq (or string) with all occurences of item removed.""" + """Return a copy of seq (or string) with all occurrences of item removed.""" if isinstance(seq, str): return seq.replace(item, '') else: @@ -71,9 +73,23 @@ def is_in(elt, seq): """Similar to (elt in seq), but compares with 'is', not '=='.""" return any(x is elt for x in seq) + +def mode(data): + """Return the most common data item. If there are ties, return any one of them.""" + [(item, count)] = collections.Counter(data).most_common(1) + return item + + +def powerset(iterable): + """powerset([1,2,3]) --> (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)""" + s = list(iterable) + return list(chain.from_iterable(combinations(s, r) for r in range(len(s)+1)))[1:] + + # ______________________________________________________________________________ # argmin and argmax + identity = lambda x: x argmin = min @@ -99,6 +115,8 @@ def shuffled(iterable): # ______________________________________________________________________________ # Statistical and mathematical functions + + def histogram(values, mode=0, bin_function=None): """Return a list of (value, count) pairs, summarizing the input values. Sorted by increasing value, or if mode=1, by decreasing count. @@ -129,7 +147,7 @@ def element_wise_product(X, Y): def matrix_multiplication(X_M, *Y_M): - """Return a matrix as a matrix-multiplication of X_M and arbitary number of matrices *Y_M""" + """Return a matrix as a matrix-multiplication of X_M and arbitrary number of matrices *Y_M""" def _mat_mult(X_M, Y_M): """Return a matrix as a matrix-multiplication of two matrices X_M and Y_M @@ -177,6 +195,7 @@ def scalar_vector_product(X, Y): def scalar_matrix_product(X, Y): + """Return matrix as a product of a scalar and a matrix""" return [scalar_vector_product(X, y) for y in Y] @@ -192,11 +211,11 @@ def inverse_matrix(X): def probability(p): - "Return true with probability p." + """Return true with probability p.""" return p > random.uniform(0.0, 1.0) -def weighted_sample_with_replacement(seq, weights, n): +def weighted_sample_with_replacement(n, seq, weights): """Pick n samples from seq at random, with replacement, with the probability of each element in proportion to its corresponding weight.""" @@ -206,7 +225,7 @@ def weighted_sample_with_replacement(seq, weights, n): def weighted_sampler(seq, weights): - "Return a random-sample function that picks from seq weighted by weights." + """Return a random-sample function that picks from seq weighted by weights.""" totals = [] for w in weights: totals.append(w + totals[-1] if totals else w) @@ -215,7 +234,7 @@ def weighted_sampler(seq, weights): def rounder(numbers, d=4): - "Round a single number, or sequence of numbers, to d decimal places." + """Round a single number, or sequence of numbers, to d decimal places.""" if isinstance(numbers, (int, float)): return round(numbers, d) else: @@ -225,8 +244,7 @@ def rounder(numbers, d=4): def num_or_str(x): """The argument is a string; convert to a number if - possible, or strip it. - """ + possible, or strip it.""" try: return int(x) except ValueError: @@ -248,11 +266,20 @@ def normalize(dist): return [(n / total) for n in dist] +def norm(X, n=2): + """Return the n-norm of vector X""" + return sum([x**n for x in X])**(1/n) + + def clip(x, lowest, highest): """Return x clipped to the range [lowest..highest].""" return max(lowest, min(x, highest)) +def sigmoid_derivative(value): + return value * (1 - value) + + def sigmoid(x): """Return activation value of x with sigmoid function""" return 1/(1 + math.exp(-x)) @@ -262,24 +289,82 @@ def step(x): """Return activation value of x with sign function""" return 1 if x >= 0 else 0 + +def gaussian(mean, st_dev, x): + """Given the mean and standard deviation of a distribution, it returns the probability of x.""" + return 1/(math.sqrt(2*math.pi)*st_dev)*math.e**(-0.5*(float(x-mean)/st_dev)**2) + + try: # math.isclose was added in Python 3.5; but we might be in 3.4 from math import isclose except ImportError: def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): - "Return true if numbers a and b are close to each other." + """Return true if numbers a and b are close to each other.""" return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + +def weighted_choice(choices): + """A weighted version of random.choice""" + # NOTE: Shoule be replaced by random.choices if we port to Python 3.6 + + total = sum(w for _, w in choices) + r = random.uniform(0, total) + upto = 0 + for c, w in choices: + if upto + w >= r: + return c, w + upto += w + + # ______________________________________________________________________________ -# Misc Functions +# Grid Functions -# TODO: Use functools.lru_cache memoization decorator +orientations = EAST, NORTH, WEST, SOUTH = [(1, 0), (0, 1), (-1, 0), (0, -1)] +turns = LEFT, RIGHT = (+1, -1) -def memoize(fn, slot=None): +def turn_heading(heading, inc, headings=orientations): + return headings[(headings.index(heading) + inc) % len(headings)] + + +def turn_right(heading): + return turn_heading(heading, RIGHT) + + +def turn_left(heading): + return turn_heading(heading, LEFT) + + +def distance(a, b): + """The distance between two (x, y) points.""" + xA, yA = a + xB, yB = b + return math.hypot((xA - xB), (yA - yB)) + + +def distance_squared(a, b): + """The square of the distance between two (x, y) points.""" + xA, yA = a + xB, yB = b + return (xA - xB)**2 + (yA - yB)**2 + + +def vector_clip(vector, lowest, highest): + """Return vector, except if any element is less than the corresponding + value of lowest or more than the corresponding value of highest, clip to + those values.""" + return type(vector)(map(clip, vector, lowest, highest)) + + +# ______________________________________________________________________________ +# Misc Functions + + +def memoize(fn, slot=None, maxsize=32): """Memoize fn: make it remember the computed value for any argument list. If slot is specified, store result in that slot of first argument. - If slot is false, store results in a dictionary.""" + If slot is false, use lru_cache for caching the values.""" if slot: def memoized_fn(obj, *args): if hasattr(obj, slot): @@ -289,37 +374,34 @@ def memoized_fn(obj, *args): setattr(obj, slot, val) return val else: + @functools.lru_cache(maxsize=maxsize) def memoized_fn(*args): - if args not in memoized_fn.cache: - memoized_fn.cache[args] = fn(*args) - return memoized_fn.cache[args] - - memoized_fn.cache = {} + return fn(*args) return memoized_fn def name(obj): - "Try to find some reasonable name for the object." + """Try to find some reasonable name for the object.""" return (getattr(obj, 'name', 0) or getattr(obj, '__name__', 0) or getattr(getattr(obj, '__class__', 0), '__name__', 0) or str(obj)) def isnumber(x): - "Is x a number?" + """Is x a number?""" return hasattr(x, '__int__') def issequence(x): - "Is x a sequence?" + """Is x a sequence?""" return isinstance(x, collections.abc.Sequence) -def print_table(table, header=None, sep=' ', numfmt='%g'): +def print_table(table, header=None, sep=' ', numfmt='{}'): """Print a list of lists as a table, so that columns line up nicely. header, if specified, will be printed as the first row. - numfmt is the format for all numbers; you might want e.g. '%6.2f'. + numfmt is the format for all numbers; you might want e.g. '{:.2f}'. (If you want different formats in different columns, don't use print_table.) sep is the separator between columns.""" justs = ['rjust' if isnumber(x) else 'ljust' for x in table[0]] @@ -339,18 +421,21 @@ def print_table(table, header=None, sep=' ', numfmt='%g'): str(x), j)(size) for (j, size, x) in zip(justs, sizes, row))) -def AIMAFile(components, mode='r'): - "Open a file based at the AIMA root directory." +def open_data(name, mode='r'): aima_root = os.path.dirname(__file__) - - aima_file = os.path.join(aima_root, *components) + aima_file = os.path.join(aima_root, *['aima-data', name]) return open(aima_file) -def DataFile(name, mode='r'): - "Return a file in the AIMA /aima-data directory." - return AIMAFile(['aima-data', name], mode) +def failure_test(algorithm, tests): + """Grades the given algorithm based on how many tests it passes. + Most algorithms have arbitrary output on correct execution, which is difficult + to check for correctness. On the other hand, a lot of algorithms output something + particular on fail (for example, False, or None). + tests is a list with each element in the form: (values, failure_output).""" + from statistics import mean + return mean(int(algorithm(x) != y) for x, y in tests) # ______________________________________________________________________________ @@ -364,49 +449,106 @@ class Expr(object): op is a str like '+' or 'sin'; args are Expressions. Expr('x') or Symbol('x') creates a symbol (a nullary Expr). Expr('-', x) creates a unary; Expr('+', x, 1) creates a binary.""" + def __init__(self, op, *args): self.op = str(op) self.args = args # Operator overloads - def __neg__(self): return Expr('-', self) - def __pos__(self): return Expr('+', self) - def __invert__(self): return Expr('~', self) - def __add__(self, rhs): return Expr('+', self, rhs) - def __sub__(self, rhs): return Expr('-', self, rhs) - def __mul__(self, rhs): return Expr('*', self, rhs) - def __pow__(self, rhs): return Expr('**',self, rhs) - def __mod__(self, rhs): return Expr('%', self, rhs) - def __and__(self, rhs): return Expr('&', self, rhs) - def __xor__(self, rhs): return Expr('^', self, rhs) - def __rshift__(self, rhs): return Expr('>>', self, rhs) - def __lshift__(self, rhs): return Expr('<<', self, rhs) - def __truediv__(self, rhs): return Expr('/', self, rhs) - def __floordiv__(self, rhs): return Expr('//', self, rhs) - def __matmul__(self, rhs): return Expr('@', self, rhs) + def __neg__(self): + return Expr('-', self) + + def __pos__(self): + return Expr('+', self) + + def __invert__(self): + return Expr('~', self) + + def __add__(self, rhs): + return Expr('+', self, rhs) + + def __sub__(self, rhs): + return Expr('-', self, rhs) + + def __mul__(self, rhs): + return Expr('*', self, rhs) + + def __pow__(self, rhs): + return Expr('**', self, rhs) + + def __mod__(self, rhs): + return Expr('%', self, rhs) + + def __and__(self, rhs): + return Expr('&', self, rhs) + + def __xor__(self, rhs): + return Expr('^', self, rhs) + + def __rshift__(self, rhs): + return Expr('>>', self, rhs) + + def __lshift__(self, rhs): + return Expr('<<', self, rhs) + + def __truediv__(self, rhs): + return Expr('/', self, rhs) + + def __floordiv__(self, rhs): + return Expr('//', self, rhs) + + def __matmul__(self, rhs): + return Expr('@', self, rhs) def __or__(self, rhs): - "Allow both P | Q, and P |'==>'| Q." + """Allow both P | Q, and P |'==>'| Q.""" if isinstance(rhs, Expression): return Expr('|', self, rhs) else: return PartialExpr(rhs, self) # Reverse operator overloads - def __radd__(self, lhs): return Expr('+', lhs, self) - def __rsub__(self, lhs): return Expr('-', lhs, self) - def __rmul__(self, lhs): return Expr('*', lhs, self) - def __rdiv__(self, lhs): return Expr('/', lhs, self) - def __rpow__(self, lhs): return Expr('**', lhs, self) - def __rmod__(self, lhs): return Expr('%', lhs, self) - def __rand__(self, lhs): return Expr('&', lhs, self) - def __rxor__(self, lhs): return Expr('^', lhs, self) - def __ror__(self, lhs): return Expr('|', lhs, self) - def __rrshift__(self, lhs): return Expr('>>', lhs, self) - def __rlshift__(self, lhs): return Expr('<<', lhs, self) - def __rtruediv__(self, lhs): return Expr('/', lhs, self) - def __rfloordiv__(self, lhs): return Expr('//', lhs, self) - def __rmatmul__(self, lhs): return Expr('@', lhs, self) + def __radd__(self, lhs): + return Expr('+', lhs, self) + + def __rsub__(self, lhs): + return Expr('-', lhs, self) + + def __rmul__(self, lhs): + return Expr('*', lhs, self) + + def __rdiv__(self, lhs): + return Expr('/', lhs, self) + + def __rpow__(self, lhs): + return Expr('**', lhs, self) + + def __rmod__(self, lhs): + return Expr('%', lhs, self) + + def __rand__(self, lhs): + return Expr('&', lhs, self) + + def __rxor__(self, lhs): + return Expr('^', lhs, self) + + def __ror__(self, lhs): + return Expr('|', lhs, self) + + def __rrshift__(self, lhs): + return Expr('>>', lhs, self) + + def __rlshift__(self, lhs): + return Expr('<<', lhs, self) + + def __rtruediv__(self, lhs): + return Expr('/', lhs, self) + + def __rfloordiv__(self, lhs): + return Expr('//', lhs, self) + + def __rmatmul__(self, lhs): + return Expr('@', lhs, self) def __call__(self, *args): "Call: if 'f' is a Symbol, then f(0) == Expr('f', 0)." @@ -438,22 +580,23 @@ def __repr__(self): # An 'Expression' is either an Expr or a Number. # Symbol is not an explicit type; it is any Expr with 0 args. + Number = (int, float, complex) Expression = (Expr, Number) def Symbol(name): - "A Symbol is just an Expr with no args." + """A Symbol is just an Expr with no args.""" return Expr(name) def symbols(names): - "Return a tuple of Symbols; names is a comma/whitespace delimited str." + """Return a tuple of Symbols; names is a comma/whitespace delimited str.""" return tuple(Symbol(name) for name in names.replace(',', ' ').split()) def subexpressions(x): - "Yield the subexpressions of an Expression (including x itself)." + """Yield the subexpressions of an Expression (including x itself).""" yield x if isinstance(x, Expr): for arg in x.args: @@ -461,7 +604,7 @@ def subexpressions(x): def arity(expression): - "The number of sub-expressions in this expression." + """The number of sub-expressions in this expression.""" if isinstance(expression, Expr): return len(expression.args) else: # expression is a number @@ -472,9 +615,14 @@ def arity(expression): class PartialExpr: """Given 'P |'==>'| Q, first form PartialExpr('==>', P), then combine with Q.""" - def __init__(self, op, lhs): self.op, self.lhs = op, lhs - def __or__(self, rhs): return Expr(self.op, self.lhs, rhs) - def __repr__(self): return "PartialExpr('{}', {})".format(self.op, self.lhs) + def __init__(self, op, lhs): + self.op, self.lhs = op, lhs + + def __or__(self, rhs): + return Expr(self.op, self.lhs, rhs) + + def __repr__(self): + return "PartialExpr('{}', {})".format(self.op, self.lhs) def expr(x): @@ -490,6 +638,7 @@ def expr(x): else: return x + infix_ops = '==> <== <=>'.split() @@ -513,10 +662,37 @@ def __missing__(self, key): return result +class hashabledict(dict): + """Allows hashing by representing a dictionary as tuple of key:value pairs + May cause problems as the hash value may change during runtime + """ + def __tuplify__(self): + return tuple(sorted(self.items())) + + def __hash__(self): + return hash(self.__tuplify__()) + + def __lt__(self, odict): + assert isinstance(odict, hashabledict) + return self.__tuplify__() < odict.__tuplify__() + + def __gt__(self, odict): + assert isinstance(odict, hashabledict) + return self.__tuplify__() > odict.__tuplify__() + + def __le__(self, odict): + assert isinstance(odict, hashabledict) + return self.__tuplify__() <= odict.__tuplify__() + + def __ge__(self, odict): + assert isinstance(odict, hashabledict) + return self.__tuplify__() >= odict.__tuplify__() + + # ______________________________________________________________________________ # Queues: Stack, FIFOQueue, PriorityQueue -# TODO: Possibly use queue.Queue, queue.PriorityQueue +# TODO: queue.PriorityQueue # TODO: Priority queues may not belong here -- see treatment in search.py @@ -552,29 +728,32 @@ class FIFOQueue(Queue): """A First-In-First-Out Queue.""" - def __init__(self): - self.A = [] - self.start = 0 + def __init__(self, maxlen=None, items=[]): + self.queue = collections.deque(items, maxlen) def append(self, item): - self.A.append(item) - - def __len__(self): - return len(self.A) - self.start + if not self.queue.maxlen or len(self.queue) < self.queue.maxlen: + self.queue.append(item) + else: + raise Exception('FIFOQueue is full') def extend(self, items): - self.A.extend(items) + if not self.queue.maxlen or len(self.queue) + len(items) <= self.queue.maxlen: + self.queue.extend(items) + else: + raise Exception('FIFOQueue max length exceeded') def pop(self): - e = self.A[self.start] - self.start += 1 - if self.start > 5 and self.start > len(self.A) / 2: - self.A = self.A[self.start:] - self.start = 0 - return e + if len(self.queue) > 0: + return self.queue.popleft() + else: + raise Exception('FIFOQueue is empty') + + def __len__(self): + return len(self.queue) def __contains__(self, item): - return item in self.A[self.start:] + return item in self.queue class PriorityQueue(Queue): @@ -614,6 +793,7 @@ def __delitem__(self, key): if item == key: self.A.pop(i) + # ______________________________________________________________________________ # Useful Shorthands @@ -622,5 +802,6 @@ class Bool(int): """Just like `bool`, except values display as 'T' and 'F' instead of 'True' and 'False'""" __str__ = __repr__ = lambda self: 'T' if self else 'F' + T = Bool(True) F = Bool(False) From 26d3d3f4728057ea56409431817bc2718d05cbc4 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Tue, 17 Apr 2018 11:13:08 -0400 Subject: [PATCH 11/40] Domain file parsing complete. Working on problem file parsing. --- parse.py | 362 +++++++++++++++++++++++++++++++++++++++++++++++++++- planning.py | 222 ++------------------------------ 2 files changed, 373 insertions(+), 211 deletions(-) diff --git a/parse.py b/parse.py index 536ef2b5e..6201c5493 100644 --- a/parse.py +++ b/parse.py @@ -1,3 +1,8 @@ +from collections.abc import MutableSequence +from planning import PlanningAction +from utils import expr + + Symbol = str # A Lisp Symbol is implemented as a Python str List = list # A Lisp List is implemented as a Python list @@ -32,7 +37,7 @@ def parse(pddl): def tokenize(s): """Convert a string into a list of tokens.""" - return s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' : ').split() + return s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split() def read_from_tokens(tokens): @@ -52,3 +57,358 @@ def read_from_tokens(tokens): raise SyntaxError('unexpected )') else: return token + + +def parse_tokens(match_dict, token_list): + def match_tokens(tokens): + if not tokens: + return False + item = tokens.pop() + if isinstance(item, MutableSequence): + match_tokens(item) + else: + for text in match_dict: + if item.startswith(text): + if match_dict[text](tokens): + break + return True + + while True: + if not match_tokens(token_list): + break + + +def parse_variables(tokens, types) -> list: + variables = [] + num_tokens = len(tokens) + idx = 0 + while idx < num_tokens: + if not tokens[idx].startswith('?'): + raise ParseError("Unrecognized variable name ({0}) " + + "that doesn't begin with a question mark".format(tokens[idx])) + pred_var = tokens[idx][1:] + if not types: + variables.append(pred_var) + idx += 1 + else: + # lookahead to see if there's a dash indicating an upcoming type name + if tokens[idx + 1] == '-': + pred_type = tokens[idx + 2].lower() + if pred_type not in types: + raise ParseError("Predicate type {0} not in type list.".format(pred_type)) + else: + pred_type = None + arg = [pred_var, pred_type] + variables.append(arg) + # if any immediately prior variables didn't have an assigned type, then assign them this one. + for j in range(len(variables) - 1, 0, -1): + if variables[j][1] is not None: + break + else: + variables[j][1] = pred_type + idx += 3 + return variables + + +def build_expr_string(expr_name, variables): + estr = expr_name + '(' + vlen = len(variables) + if vlen: + for i in range(vlen - 1): + estr += variables[i] + ', ' + estr += variables[vlen - 1] + estr += ')' + return estr + + +class PDDLDomainParser: + def __init__(self): + self.domain_name = '' + self.action_name = '' + self.requirements = [] + self.predicates = [] + self.actions = [] + self.types = [] + self.constants = [] + self.parameters = [] + self.preconditions = [] + self.effects = [] + + def _parse_define(self, tokens) -> bool: + domain_list = tokens.pop() + token = domain_list.pop() + if token != 'domain': + raise ParseError('domain keyword not found after define statement') + self.domain_name = domain_list.pop() + return True + + def _parse_requirements(self, tokens) -> bool: + self.requirements = tokens + if ':strips' not in self.requirements: + raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') + return True + + def _parse_constants(self, tokens) -> bool: + self.constants = parse_variables(tokens, self.types) + for const, ctype in self.constants: + if ctype not in self.types: + raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) + return True + + def _parse_types(self, tokens) -> bool: + self.types = tokens + return True + + def _parse_predicates(self, tokens) -> bool: + while tokens: + predicate = tokens.pop() + pred_name = predicate.pop() + predicate.reverse() + new_predicate = [pred_name] + parse_variables(predicate, self.types) + self.predicates.append(new_predicate) + return True + + def _parse_action(self, tokens) -> bool: + self.action_name = tokens.pop() + self.parameters = [] + self.preconditions = [] + self.effects = [] + match = {':parameters': self._parse_parameters, + ':precondition': self._parse_precondition, + ':effect': self._parse_effect + } + parse_tokens(match, tokens) + params = [p[0] for p in self.parameters] + action_str = build_expr_string(self.action_name, params) + action = PlanningAction(expr(action_str), self.preconditions, self.effects) + self.actions.append(action) + return True + + def _parse_parameters(self, tokens) -> bool: + param_list = tokens.pop() + param_list.reverse() + self.parameters = parse_variables(param_list, self.types) + return True + + def _parse_single_expr_string(self, tokens) -> str: + if tokens[0] == 'not': + token = tokens.pop() + token.reverse() + e = self._parse_single_expr_string(token) + if '~' in e: + raise ParseError('Multiple not operators in expression.') + return '~' + e + else: + expr_name = tokens[0] + variables = [] + idx = 1 + num_tokens = len(tokens) + while idx < num_tokens: + param = tokens[idx] + if param.startswith('?'): + variables.append(param[1:].lower()) + else: + variables.append(param) + idx += 1 + return build_expr_string(expr_name, variables) + + def _parse_expr_list(self, tokens) -> list: + expr_lst = [] + while tokens: + token = tokens.pop() + token.reverse() + e = self._parse_single_expr_string(token) + expr_lst.append(expr(e)) + return expr_lst + + def _parse_formula(self, tokens) -> list: + expr_lst = [] + token = tokens.pop() + if token == 'and': # preconds and effects only use 'and' keyword + exprs = self._parse_expr_list(tokens) + expr_lst.extend(exprs) + else: # parse single expression + e = self._parse_single_expr_string([token] + tokens) + expr_lst.append(expr(e)) + return expr_lst + + def _parse_precondition(self, tokens) -> bool: + precond_list = tokens.pop() + self.preconditions = self._parse_formula(precond_list) + return True + + def _parse_effect(self, tokens) -> bool: + effects_list = tokens.pop() + self.effects = self._parse_formula(effects_list) + return True + + def read(self, filename) -> None: + pddl_list = read_pddl_file(filename) + + # 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_list) + + +class PDDLProblemParser: + def __init__(self, types): + self.problem_name = '' + self.types = types + self.objects = [] + self.init = [] + self.goal = [] + + def _parse_define(self, tokens) -> bool: + problem_list = tokens.pop() + token = problem_list.pop() + if token != 'problem': + raise ParseError('problem keyword not found after define statement') + self.problem_name = problem_list.pop() + return True + + def _parse_objects(self, tokens) -> bool: + self.objects = parse_variables(tokens) + for const, ctype in self.constants: + if ctype not in self.types: + raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) + return True + + def _parse_types(self, tokens) -> bool: + self.types = tokens + return True + + def _parse_predicates(self, tokens) -> bool: + while tokens: + predicate = tokens.pop() + pred_name = predicate.pop() + predicate.reverse() + new_predicate = [pred_name] + self._parse_variables(predicate) + self.predicates.append(new_predicate) + return True + + def _parse_variables(self, tokens) -> list: + variables = [] + num_tokens = len(tokens) + idx = 0 + while idx < num_tokens: + if not tokens[idx].startswith('?'): + raise ParseError("Unrecognized variable name ({0}) " + + "that doesn't begin with a question mark".format(tokens[idx])) + pred_var = tokens[idx][1:] + if not self.types: + variables.append(pred_var) + idx += 1 + else: + # lookahead to see if there's a dash indicating an upcoming type name + if tokens[idx + 1] == '-': + pred_type = tokens[idx + 2].lower() + if pred_type not in self.types: + raise ParseError("Predicate type {0} not in type list.".format(pred_type)) + else: + pred_type = None + arg = [pred_var, pred_type] + variables.append(arg) + # if any immediately prior variables didn't have an assigned type, then assign them this one. + for j in range(len(variables) - 1, 0, -1): + if variables[j][1] is not None: + break + else: + variables[j][1] = pred_type + idx += 3 + return variables + + def _parse_action(self, tokens) -> bool: + self.action_name = tokens.pop() + self.parameters = [] + self.preconditions = [] + self.effects = [] + match = {':parameters': self._parse_parameters, + ':precondition': self._parse_precondition, + ':effect': self._parse_effect + } + parse_tokens(match, tokens) + params = [p[0] for p in self.parameters] + action_str = build_expr_string(self.action_name, params) + action = PlanningAction(expr(action_str), self.preconditions, self.effects) + self.actions.append(action) + return True + + def _parse_parameters(self, tokens) -> bool: + param_list = tokens.pop() + param_list.reverse() + self.parameters = self._parse_variables(param_list) + return True + + def _parse_single_expr_string(self, tokens): + if tokens[0] == 'not': + token = tokens.pop() + token.reverse() + e = self._parse_single_expr_string(token) + if '~' in e: + raise ParseError('Multiple not operators in expression.') + return '~' + e + else: + expr_name = tokens[0] + variables = [] + idx = 1 + num_tokens = len(tokens) + while idx < num_tokens: + param = tokens[idx] + if param.startswith('?'): + variables.append(param[1:].lower()) + else: + variables.append(param) + idx += 1 + return build_expr_string(expr_name, variables) + + def _parse_expr_list(self, tokens): + expr_lst = [] + while tokens: + token = tokens.pop() + token.reverse() + e = self._parse_single_expr_string(token) + expr_lst.append(expr(e)) + return expr_lst + + def _parse_formula(self, tokens): + expr_lst = [] + token = tokens.pop() + if token == 'and': # preconds and effects only use 'and' keyword + exprs = self._parse_expr_list(tokens) + expr_lst.extend(exprs) + else: # parse single expression + e = self._parse_single_expr_string([token] + tokens) + expr_lst.append(expr(e)) + return expr_lst + + def _parse_precondition(self, tokens): + precond_list = tokens.pop() + self.preconditions = self._parse_formula(precond_list) + return True + + def _parse_effect(self, tokens): + effects_list = tokens.pop() + self.effects = self._parse_formula(effects_list) + return True + + def read(self, filename): + pddl_list = read_pddl_file(filename) + + # 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, + ':objects': self._parse_objects, + ':init': self._parse_init, + ':goal': self._parse_goal + } + + parse_tokens(match, pddl_list) diff --git a/planning.py b/planning.py index 0d1f4072b..17de3ec28 100644 --- a/planning.py +++ b/planning.py @@ -3,8 +3,7 @@ from logic import fol_bc_and from utils import expr, Expr, partition from search import astar_search -from parse import read_pddl_file, ParseError -from collections.abc import MutableSequence +from parse import PDDLDomainParser, PDDLProblemParser class PlanningKB: @@ -175,210 +174,6 @@ def act(self, subst, kb): return new_kb -class PDDLDomainParser: - def __init__(self): - self.domain_name = '' - self.action_name = '' - self.tokens = [] - self.requirements = [] - self.predicates = [] - self.actions = [] - self.types = [] - self.constants = [] - self.parameters = [] - self.preconditions = [] - self.effects = [] - - def _parse_define(self, tokens) -> bool: - domain_list = tokens.pop() - token = domain_list.pop() - if token != 'domain': - raise ParseError('domain keyword not found after define statement') - self.domain_name = domain_list.pop() - return True - - def _parse_requirements(self, tokens) -> bool: - self.requirements = tokens - if ':strips' not in self.requirements: - raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') - return True - - def _parse_constants(self, tokens) -> bool: - self.constants = self._parse_variables(tokens) - for const, ctype in self.constants: - if ctype not in self.types: - raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) - return True - - def _parse_types(self, tokens) -> bool: - self.types = tokens - return True - - def _parse_predicates(self, tokens) -> bool: - while tokens: - predicate = tokens.pop() - predicate.reverse() - new_predicate = [predicate[0]] + self._parse_variables(predicate) - self.predicates.append(new_predicate) - return True - - def _parse_variables(self, tokens) -> list: - variables = [] - num_tokens = len(tokens) - idx = 1 - while idx < num_tokens: - if not tokens[idx].startswith('?'): - raise ParseError("Unrecognized variable name ({0}) " + - "that doesn't begin with a question mark".format(tokens[idx])) - pred_var = tokens[idx][1:] - if not self.types: - variables.append(pred_var) - idx += 1 - else: - # lookahead to see if there's a dash indicating an upcoming type name - if tokens[idx + 1] == '-': - pred_type = tokens[idx + 2].lower() - if pred_type not in self.types: - raise ParseError("Predicate type {0} not in type list.".format(pred_type)) - else: - pred_type = None - arg = [pred_var, pred_type] - variables.append(arg) - # if any immediately prior variables didn't have an assigned type, then assign them this one. - for j in range(len(variables) - 1, 0, -1): - if variables[j][1] is not None: - break - else: - variables[j][1] = pred_type - idx += 3 - return variables - - def _parse_action(self, tokens) -> bool: - self.action_name = self.tokens[idx].lower() - idx += 1 - match = {':parameters': self._parse_parameters, - ':precondition': self._parse_precondition, - ':effect': self._parse_effect - } - idx = self.match_and_parse_tokens(idx, match) - return True - - def _parse_parameters(self, tokens) -> bool: - idx += 1 - if self.tokens[idx] != '(': - raise IOError('Start of parameter list is missing an open parenthesis.') - self.parameters.clear() - while idx < self.num_tokens: - if self.tokens[idx] == ')': - self.num_parens -= 1 - break - elif self.tokens[idx] == '(': - self.num_parens += 1 - try: - param_vars, idx = self._parse_variables(idx+1) - except IOError: - raise IOError('Action name {0} has an invalid argument list.'.format(self.action_name)) - self.parameters.extend(param_vars) - return True - - def _parse_single_expr(self, idx): - if self.tokens[idx+1] == 'not': - e = self._parse_single_expr(idx + 2) - if '~' in e: - raise IOError('Multiple not operators in expression.') - return expr('~' + e) - else: - if self.tokens[idx] != '(': - raise IOError('Expression in {0} is missing an open parenthesis.'.format(self.action_name)) - while idx < self.num_tokens: - if self.tokens[idx] == ')': - self.num_parens -= 1 - idx += 1 - break - elif self.tokens[idx] == '(': - expr_name = self.tokens[idx + 1] - variables = [] - idx += 2 - while idx < self.num_tokens: - if self.tokens[idx] == ')': - self.num_parens -= 1 - break - param = self.tokens[idx] - if param.startswith('?'): - variables.append(param.lower()) - else: - variables.append(param) - estr = expr_name + '(' - vlen = len(variables) - for i in range(vlen - 1): - estr += variables[i] + ', ' - estr += variables[vlen-1] + ')' - return estr - - def _parse_expr_list(self, idx): - expr_lst = [] - while idx < self.num_tokens: - if self.tokens[idx] == ')': - self.num_parens -= 1 - break - elif self.tokens[idx] == '(': - idx, expr = self._parse_single_expr(idx) - expr_lst.append(expr) - idx += 1 - return expr_lst - - def _parse_formula(self, idx, label): - expr_lst = [] - idx += 1 - if self.tokens[idx] == '(': - self.num_parens += 1 - else: - raise IOError('Start of {0} {1} is missing an open parenthesis.'.format(self.action_name, label)) - if self.tokens[idx + 1] == 'and': # preconds and effects only use 'and' keyword - exprs = self._parse_expr_list(idx + 2) - expr_lst.extend(exprs) - else: # parse single expression - expr = self._parse_single_expr(idx + 2) - expr_lst.append(expr) - return expr_lst - - def _parse_precondition(self, tokens): - idx, self.preconditions = self._parse_formula(idx, 'preconditions') - return True - - def _parse_effect(self, tokens): - idx, self.effects = self._parse_formula(idx, 'effects') - return True - - def read(self, filename): - pddl_list = read_pddl_file(filename) - - # 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 - } - - def parse_tokens(tokens): - if not tokens: - return - item = tokens.pop() - if isinstance(item, MutableSequence): - parse_tokens(item) - else: - for text in match: - if item.startswith(text): - if match[text](tokens): - break - - while True: - parse_tokens(pddl_list) - - def print_solution(node): for action in node.solution(): print(action.name, end='(') @@ -541,9 +336,15 @@ def put_on_shoes(): print_solution(astar_search(p)) -def parse_domain_file(filename): - parser = PDDLDomainParser() - parser.read(filename) +def parse_domain_file(filename) -> PDDLDomainParser: + return parser + + +def solution_from_PDDL_files(domain_file, problem_file) -> None: + domain_parser = PDDLDomainParser() + domain_parser.read(domain_file) + problem_parser = PDDLProblemParser(domain_parser.types) + problem_parser.read(problem_file) def tester(): @@ -557,7 +358,8 @@ def tester(): sussman_anomaly() print('\nPut on shoes solution:') put_on_shoes() - parse_domain_file('blocks-domain.pddl') + print('\nBlocks solution via PDDL:') + solution_from_PDDL_files('blocks-domain.pddl', 'blocks-problem.pddl') if __name__ == '__main__': From 233be503464dfe1d0b0f133a31bc8c6ef6f92e07 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Fri, 20 Apr 2018 07:17:40 -0400 Subject: [PATCH 12/40] Added blocks world PDDL to the repo. More than likely will need to move this to a subfolder eventually. --- blocks-domain.pddl | 49 +++++++++++++++++++++++++++++++++++++++++++++ blocks-problem.pddl | 18 +++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100755 blocks-domain.pddl create mode 100755 blocks-problem.pddl diff --git a/blocks-domain.pddl b/blocks-domain.pddl new file mode 100755 index 000000000..c553461b4 --- /dev/null +++ b/blocks-domain.pddl @@ -0,0 +1,49 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; 4 Op-blocks world +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (domain BLOCKS) + (:requirements :strips :typing) + (:types block) + (:predicates (on ?x - block ?y - block) + (ontable ?x - block) + (clear ?x - block) + (handempty) + (holding ?x - block) + ) + + (:action pick-up + :parameters (?x - block) + :precondition (and (clear ?x) (ontable ?x) (handempty)) + :effect + (and (not (ontable ?x)) + (not (clear ?x)) + (not (handempty)) + (holding ?x))) + + (:action put-down + :parameters (?x - block) + :precondition (holding ?x) + :effect + (and (not (holding ?x)) + (clear ?x) + (handempty) + (ontable ?x))) + (:action stack + :parameters (?x - block ?y - block) + :precondition (and (holding ?x) (clear ?y)) + :effect + (and (not (holding ?x)) + (not (clear ?y)) + (clear ?x) + (handempty) + (on ?x ?y))) + (:action unstack + :parameters (?x - block ?y - block) + :precondition (and (on ?x ?y) (clear ?x) (handempty)) + :effect + (and (holding ?x) + (clear ?y) + (not (clear ?x)) + (not (handempty)) + (not (on ?x ?y))))) diff --git a/blocks-problem.pddl b/blocks-problem.pddl new file mode 100755 index 000000000..76b5155c0 --- /dev/null +++ b/blocks-problem.pddl @@ -0,0 +1,18 @@ +(define (problem BLOCKS-4-0) + +(:domain BLOCKS) + +(:objects D B A C - block) + +(:INIT (CLEAR C) + (CLEAR A) + (CLEAR B) + (CLEAR D) + (ONTABLE C) + (ONTABLE A) + (ONTABLE B) + (ONTABLE D) + (HANDEMPTY)) + +(:goal (AND (ON D C) (ON C B) (ON B A))) +) From 5b6f9c4f42e422e6e0e7beb5fef108852f71dbf7 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 21 Apr 2018 21:37:51 -0400 Subject: [PATCH 13/40] Renamed parse.py to pddl_parse and updated imports. --- pddl_parse.py | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++ planning.py | 10 +- 2 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 pddl_parse.py diff --git a/pddl_parse.py b/pddl_parse.py new file mode 100644 index 000000000..1216f53ce --- /dev/null +++ b/pddl_parse.py @@ -0,0 +1,281 @@ +from typing import Deque +from collections import deque +from planning import PlanningAction +from utils import expr + + +Symbol = str # A Lisp Symbol is implemented as a Python str +List = list # A Lisp List is implemented as a Python list + + +class ParseError(Exception): + pass + + +def read_pddl_file(filename) -> list: + with open(filename) 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 + s = ''.join(lines) + # transform into Python-compatible S-expressions (using lists of strings) + return parse(s) + + +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(pddl): + """Read PDDL contained in a string.""" + return read_from_tokens(tokenize(pddl)) + + +def tokenize(s: str) -> deque: + """Convert a string into a list of tokens.""" + return deque(s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split()) + + +def read_from_tokens(tokens: deque): + """Read an expression from a sequence of tokens.""" + if len(tokens) == 0: + raise SyntaxError('unexpected EOF while reading') + token = tokens.popleft() + if '(' == token: + D = deque() + while tokens[0] != ')': + D.appendleft(read_from_tokens(tokens)) + tokens.popleft() # pop off ')' + return D + elif ')' == token: + raise SyntaxError('unexpected )') + else: + return token + + +def parse_tokens(match_dict, token_list): + def match_tokens(tokens): + if not tokens: + return False + item = tokens.popleft() + if isinstance(item, Deque): + match_tokens(item) + else: + item = item.lower() + for text in match_dict: + if item.startswith(text): + if match_dict[text](tokens): + break + return True + + while True: + if not match_tokens(token_list): + break + + +def parse_variables(tokens, types) -> list: + """ Extracts a list of variables from the PDDL. """ + variables = [] + while tokens: + token = tokens.popleft() + if not token.startswith('?'): + raise ParseError("Unrecognized variable name ({0}) " + + "that doesn't begin with a question mark".format(token)) + pred_var = token[1:] + if 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 + tokens.popleft() + tokens.popleft() + variables.append(pred_var) + return variables + + +def _parse_single_expr_string(tokens: deque) -> str: + if tokens[0] == 'not': + token = tokens.popleft() + e = _parse_single_expr_string(token) + if '~' in e: + raise ParseError('Multiple not operators in expression.') + return '~' + e + else: + expr_name = tokens.popleft().lower() + variables = [] + while tokens: + param = tokens.popleft() + if param.startswith('?'): + variables.append(param[1:].lower()) + else: + variables.append(param) + return build_expr_string(expr_name, variables) + + +def _parse_expr_list(tokens) -> list: + expr_lst = [] + while tokens: + token = tokens.popleft() + e = _parse_single_expr_string(token) + expr_lst.append(expr(e)) + return expr_lst + + +def parse_formula(tokens: deque) -> list: + expr_lst = [] + token = tokens.popleft() + if token.lower() == 'and': # preconds and effects only use 'and' keyword + exprs = _parse_expr_list(tokens) + expr_lst.extend(exprs) + else: # parse single expression + e = _parse_single_expr_string([token] + tokens) + expr_lst.append(expr(e)) + return expr_lst + + +def build_expr_string(expr_name: str, variables: list) -> str: + estr = expr_name + '(' + vlen = len(variables) + if vlen: + for i in range(vlen - 1): + estr += variables[i] + ', ' + estr += variables[vlen - 1] + estr += ')' + return estr + + +class PDDLDomainParser: + def __init__(self): + self.domain_name = '' + self.action_name = '' + self.requirements = [] + self.predicates = [] + self.actions = [] + self.types = [] + self.constants = [] + self.parameters = [] + self.preconditions = [] + self.effects = [] + + def _parse_define(self, tokens: deque) -> bool: + domain_list = tokens.popleft() + token = domain_list.popleft() + if token != 'domain': + raise ParseError('domain keyword not found after define statement') + self.domain_name = domain_list.popleft() + return True + + def _parse_requirements(self, tokens: deque) -> bool: + self.requirements = list(tokens) + if ':strips' not in self.requirements: + raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') + return True + + def _parse_constants(self, tokens: deque) -> bool: + self.constants = parse_variables(tokens) + return True + + def _parse_types(self, tokens: deque) -> bool: + self.types = True + return True + + def _parse_predicates(self, tokens: deque) -> bool: + while tokens: + predicate = tokens.popleft() + pred_name = predicate.popleft() + new_predicate = [pred_name] + parse_variables(predicate, self.types) + self.predicates.append(new_predicate) + return True + + def _parse_action(self, tokens) -> bool: + self.action_name = tokens.pop() + self.parameters = [] + self.preconditions = [] + self.effects = [] + match = {':parameters': self._parse_parameters, + ':precondition': self._parse_precondition, + ':effect': self._parse_effect + } + parse_tokens(match, tokens) + params = [p[0] for p in self.parameters] + action_str = build_expr_string(self.action_name, params) + action = PlanningAction(expr(action_str), self.preconditions, self.effects) + self.actions.append(action) + return True + + def _parse_parameters(self, tokens: deque) -> bool: + param_list = tokens.popleft() + self.parameters = parse_variables(param_list, self.types) + return True + + def _parse_precondition(self, tokens: deque) -> bool: + precond_list = tokens.popleft() + self.preconditions = parse_formula(precond_list) + return True + + def _parse_effect(self, tokens: deque) -> bool: + effects_list = tokens.popleft() + self.effects = parse_formula(effects_list) + return True + + def read(self, filename) -> None: + pddl_list = read_pddl_file(filename) + + # 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_list) + + +class PDDLProblemParser: + def __init__(self, types): + self.problem_name = '' + self.types = types + self.objects = [] + self.init = [] + self.goal = [] + + def _parse_define(self, tokens: deque) -> bool: + problem_list = tokens.popleft() + token = problem_list.popleft() + if token != 'problem': + raise ParseError('problem keyword not found after define statement') + self.problem_name = problem_list.popleft() + return True + + def _parse_domain(self, tokens: deque) -> bool: + self.domain_name = tokens.popleft() + return True + + def _parse_init(self, tokens: deque): + self.initial_kb = _parse_expr_list(tokens) + return True + + def _parse_goal(self, tokens: deque): + goal_list = tokens.popleft() + self.goal = parse_formula(goal_list) + return True + + def read(self, filename): + pddl_list = read_pddl_file(filename) + + # 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_list) diff --git a/planning.py b/planning.py index 17de3ec28..81a0b61be 100644 --- a/planning.py +++ b/planning.py @@ -1,9 +1,9 @@ """Planning (Chapters 10-11) """ +import pddl_parse from logic import fol_bc_and from utils import expr, Expr, partition from search import astar_search -from parse import PDDLDomainParser, PDDLProblemParser class PlanningKB: @@ -336,14 +336,10 @@ def put_on_shoes(): print_solution(astar_search(p)) -def parse_domain_file(filename) -> PDDLDomainParser: - return parser - - def solution_from_PDDL_files(domain_file, problem_file) -> None: - domain_parser = PDDLDomainParser() + domain_parser = pddl_parse.PDDLDomainParser() domain_parser.read(domain_file) - problem_parser = PDDLProblemParser(domain_parser.types) + problem_parser = pddl_parse.PDDLProblemParser(domain_parser.types) problem_parser.read(problem_file) From 32bdb811dce01673369d70807a53d760749a1d8f Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 22 Apr 2018 13:09:00 -0400 Subject: [PATCH 14/40] Moved to pddl_files subfolder --- pddl_files/blocks-domain.pddl | 49 ++++++++++++++++++++++++++++++++++ pddl_files/blocks-problem.pddl | 18 +++++++++++++ 2 files changed, 67 insertions(+) create mode 100755 pddl_files/blocks-domain.pddl create mode 100755 pddl_files/blocks-problem.pddl diff --git a/pddl_files/blocks-domain.pddl b/pddl_files/blocks-domain.pddl new file mode 100755 index 000000000..c553461b4 --- /dev/null +++ b/pddl_files/blocks-domain.pddl @@ -0,0 +1,49 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; 4 Op-blocks world +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (domain BLOCKS) + (:requirements :strips :typing) + (:types block) + (:predicates (on ?x - block ?y - block) + (ontable ?x - block) + (clear ?x - block) + (handempty) + (holding ?x - block) + ) + + (:action pick-up + :parameters (?x - block) + :precondition (and (clear ?x) (ontable ?x) (handempty)) + :effect + (and (not (ontable ?x)) + (not (clear ?x)) + (not (handempty)) + (holding ?x))) + + (:action put-down + :parameters (?x - block) + :precondition (holding ?x) + :effect + (and (not (holding ?x)) + (clear ?x) + (handempty) + (ontable ?x))) + (:action stack + :parameters (?x - block ?y - block) + :precondition (and (holding ?x) (clear ?y)) + :effect + (and (not (holding ?x)) + (not (clear ?y)) + (clear ?x) + (handempty) + (on ?x ?y))) + (:action unstack + :parameters (?x - block ?y - block) + :precondition (and (on ?x ?y) (clear ?x) (handempty)) + :effect + (and (holding ?x) + (clear ?y) + (not (clear ?x)) + (not (handempty)) + (not (on ?x ?y))))) diff --git a/pddl_files/blocks-problem.pddl b/pddl_files/blocks-problem.pddl new file mode 100755 index 000000000..76b5155c0 --- /dev/null +++ b/pddl_files/blocks-problem.pddl @@ -0,0 +1,18 @@ +(define (problem BLOCKS-4-0) + +(:domain BLOCKS) + +(:objects D B A C - block) + +(:INIT (CLEAR C) + (CLEAR A) + (CLEAR B) + (CLEAR D) + (ONTABLE C) + (ONTABLE A) + (ONTABLE B) + (ONTABLE D) + (HANDEMPTY)) + +(:goal (AND (ON D C) (ON C B) (ON B A))) +) From 9a8f564146665967e30f4da12490aeb1b4a5fc99 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 22 Apr 2018 13:10:22 -0400 Subject: [PATCH 15/40] In process of updating example functions to be brought in as PDDL files instead. --- planning.py | 122 +++++++++++++++++----------------------------------- 1 file changed, 40 insertions(+), 82 deletions(-) diff --git a/planning.py b/planning.py index 81a0b61be..309d0bab7 100644 --- a/planning.py +++ b/planning.py @@ -1,9 +1,10 @@ """Planning (Chapters 10-11) """ -import pddl_parse +import os from logic import fol_bc_and from utils import expr, Expr, partition from search import astar_search +from pddl_parse import DomainParser, ProblemParser, ParseError class PlanningKB: @@ -186,42 +187,44 @@ def print_solution(node): print() -def air_cargo(): - goals = [expr('At(C1, JFK)'), expr('At(C2, SFO)')] +def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: + initial_kb = PlanningKB([expr(g) for g in pddl_problem.goals], + [expr(s) for s in pddl_problem.initial_state]) - init = PlanningKB(goals, - [expr('At(C1, SFO)'), - expr('At(C2, JFK)'), - expr('At(P1, SFO)'), - expr('At(P2, JFK)'), - expr('Cargo(C1)'), - expr('Cargo(C2)'), - expr('Plane(P1)'), - expr('Plane(P2)'), - expr('Airport(JFK)'), - expr('Airport(SFO)')]) - - # Actions - # Load - precond = [expr('At(c, a)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] - effect = [expr('In(c, p)'), expr('~At(c, a)')] - load = PlanningAction(expr('Load(c, p, a)'), precond, effect) - - # Unload - precond = [expr('In(c, p)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] - effect = [expr('At(c, a)'), expr('~In(c, p)')] - unload = PlanningAction(expr('Unload(c, p, a)'), precond, effect) - - # Fly - # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function - precond = [expr('At(p, f)'), expr('Plane(p)'), expr('Airport(f)'), expr('Airport(to)')] - effect = [expr('At(p, to)'), expr('~At(p, f)')] - fly = PlanningAction(expr('Fly(p, f, to)'), precond, effect) - - p = PlanningProblem(init, [load, unload, fly]) + planning_actions = [PlanningAction(expr(name), + [expr(p) for p in preconds], + [expr(e) for e in effects]) + for name, preconds, effects in pddl_domain.actions] + p = PlanningProblem(initial_kb, planning_actions) + print('\n{} solution:'.format(pddl_problem.problem_name)) print_solution(astar_search(p)) +def gather_test_pairs() -> list: + pddl_direntries = os.scandir(os.getcwd() + os.sep + 'pddl_files') + domain_objects = [] + problem_objects = [] + for de in pddl_direntries: + try: + domain_parser = DomainParser() + domain_parser.read(de.path) + domain_objects.append(domain_parser) + except ParseError: + try: + problem_parser = ProblemParser() + problem_parser.read(de.path) + problem_objects.append(problem_parser) + except ParseError: + raise ParseError('Unparseable PDDL file: {}'.format(de.name)) + + object_pairs = [] + for p in problem_objects: + for d in domain_objects: + if p.domain_name == d.domain_name: + object_pairs.append((d, p)) + return object_pairs + + def spare_tire(): goals = [expr('At(Spare, Axle)')] init = PlanningKB(goals, @@ -249,34 +252,6 @@ def spare_tire(): print_solution(astar_search(p)) -def three_block_tower(): - goals = [expr('On(A, B)'), expr('On(B, C)')] - init = PlanningKB(goals, - [expr('On(A, Table)'), - expr('On(B, Table)'), - expr('On(C, Table)'), - expr('Block(A)'), - expr('Block(B)'), - expr('Block(C)'), - expr('Clear(A)'), - expr('Clear(B)'), - expr('Clear(C)')]) - - # Actions - # Move(b, x, y) - precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] - effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] - move = PlanningAction(expr('Move(b, x, y)'), precond, effect) - - # MoveToTable(b, x) - precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] - effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] - move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) - - p = PlanningProblem(init, [move, move_to_table]) - print_solution(astar_search(p)) - - def sussman_anomaly(): goals = [expr('On(A, B)'), expr('On(B, C)')] init = PlanningKB(goals, @@ -336,27 +311,10 @@ def put_on_shoes(): print_solution(astar_search(p)) -def solution_from_PDDL_files(domain_file, problem_file) -> None: - domain_parser = pddl_parse.PDDLDomainParser() - domain_parser.read(domain_file) - problem_parser = pddl_parse.PDDLProblemParser(domain_parser.types) - problem_parser.read(problem_file) - - -def tester(): - print('Air cargo solution:') - air_cargo() - print('\nSpare tire solution:') - spare_tire() - print('\nThree block tower solution:') - three_block_tower() - print('\nSussman anomaly solution:') - sussman_anomaly() - print('\nPut on shoes solution:') - put_on_shoes() - print('\nBlocks solution via PDDL:') - solution_from_PDDL_files('blocks-domain.pddl', 'blocks-problem.pddl') +def test_solutions(): + for domain, problem in gather_test_pairs(): + construct_solution_from_pddl(domain, problem) if __name__ == '__main__': - tester() + test_solutions() From 57e1e964b0fda46dc76ac5181cfa26e0115b00d5 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Fri, 27 Apr 2018 10:04:25 -0400 Subject: [PATCH 16/40] Last version of my own PDDL parsing. --- pddl_parse.py | 192 ++++++++++++++++++++++++++++---------------------- 1 file changed, 109 insertions(+), 83 deletions(-) diff --git a/pddl_parse.py b/pddl_parse.py index 1216f53ce..19fcaf97b 100644 --- a/pddl_parse.py +++ b/pddl_parse.py @@ -1,9 +1,9 @@ +import os from typing import Deque from collections import deque -from planning import PlanningAction -from utils import expr - +CHAR = 0 +WHITESPACE = [' ', '\t'] Symbol = str # A Lisp Symbol is implemented as a Python str List = list # A Lisp List is implemented as a Python list @@ -12,15 +12,14 @@ class ParseError(Exception): pass -def read_pddl_file(filename) -> list: +def read_pddl_file(filename) -> deque: with open(filename) 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 - s = ''.join(lines) - # transform into Python-compatible S-expressions (using lists of strings) - return parse(s) + + # transform into Python-compatible S-expressions (using deques of strings) + return readlines(filename, lines) def strip_comments(lines) -> None: @@ -31,31 +30,53 @@ def strip_comments(lines) -> None: lines[i] = line[:idx] -def parse(pddl): +def readlines(filename: str, pddl: list) -> deque: """Read PDDL contained in a string.""" - return read_from_tokens(tokenize(pddl)) - - -def tokenize(s: str) -> deque: - """Convert a string into a list of tokens.""" - return deque(s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split()) + return parse(filename, tokenize(pddl)) -def read_from_tokens(tokens: deque): - """Read an expression from a sequence of tokens.""" +def parse(filename: str, tokens: deque): + # read the tokens one at time (left to right) and separate them into list of keywords and parameters. if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.popleft() - if '(' == token: + raise ParseError('unexpected EOF while reading {}.'.format(os.path.basename(filename))) + char, line_no, col_no = tokens.popleft() + if '(' == char: D = deque() - while tokens[0] != ')': - D.appendleft(read_from_tokens(tokens)) - tokens.popleft() # pop off ')' + s = '' + while True: + try: + if tokens[0][CHAR] == '(': + D.append(parse(filename, tokens)) + elif tokens[0][CHAR] == ')': + if s: + D.append(s) + tokens.popleft() + break + elif tokens[0][CHAR] in WHITESPACE: + if s: + D.append(s) + tokens.popleft() + s = '' + else: + char, line_no, col_no = tokens.popleft() + s += char + except IndexError: + raise ParseError('unexpected EOF while reading {}.'.format(os.path.basename(filename))) return D - elif ')' == token: - raise SyntaxError('unexpected )') + elif ')' == char: + raise ParseError("unexpected ')' token in {}, line {}, col {}".format(os.path.basename(filename), + line_no + 1, col_no + 1)) else: - return token + return char + + +def tokenize(pddl: list) -> deque: + """Convert a string into a deque of tokens.""" + d = deque() + for lineno, line in enumerate(pddl): + for column, char in enumerate(line): + d.append((char, lineno, column)) + return d def parse_tokens(match_dict, token_list): @@ -78,7 +99,7 @@ def match_tokens(tokens): break -def parse_variables(tokens, types) -> list: +def parse_variables(tokens, has_types) -> list: """ Extracts a list of variables from the PDDL. """ variables = [] while tokens: @@ -87,7 +108,7 @@ def parse_variables(tokens, types) -> list: raise ParseError("Unrecognized variable name ({0}) " + "that doesn't begin with a question mark".format(token)) pred_var = token[1:] - if types: + 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 @@ -99,13 +120,14 @@ def parse_variables(tokens, types) -> list: def _parse_single_expr_string(tokens: deque) -> str: if tokens[0] == 'not': - token = tokens.popleft() + # 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: - expr_name = tokens.popleft().lower() + else: # expression is a standard Op(param1, param2, etc ...) format + expr_name = tokens.popleft().capitalize() variables = [] while tokens: param = tokens.popleft() @@ -120,8 +142,7 @@ def _parse_expr_list(tokens) -> list: expr_lst = [] while tokens: token = tokens.popleft() - e = _parse_single_expr_string(token) - expr_lst.append(expr(e)) + expr_lst.append(_parse_single_expr_string(token)) return expr_lst @@ -132,13 +153,13 @@ def parse_formula(tokens: deque) -> list: exprs = _parse_expr_list(tokens) expr_lst.extend(exprs) else: # parse single expression - e = _parse_single_expr_string([token] + tokens) - expr_lst.append(expr(e)) + expr_lst.append(_parse_single_expr_string(deque([token]) + tokens)) return expr_lst def build_expr_string(expr_name: str, variables: list) -> str: - estr = expr_name + '(' + # can't have actions with a dash in the name; it confuses the Expr class + estr = expr_name.replace('-', '').capitalize() + '(' vlen = len(variables) if vlen: for i in range(vlen - 1): @@ -148,82 +169,88 @@ def build_expr_string(expr_name: str, variables: list) -> str: return estr -class PDDLDomainParser: +class DomainParser: def __init__(self): self.domain_name = '' - self.action_name = '' - self.requirements = [] + self._action_name = '' + self._requirements = [] self.predicates = [] self.actions = [] - self.types = [] self.constants = [] - self.parameters = [] - self.preconditions = [] - self.effects = [] + self._types = [] + self._parameters = [] + self._preconditions = [] + self._effects = [] def _parse_define(self, tokens: deque) -> bool: - domain_list = tokens.popleft() - token = domain_list.popleft() + domain_seq = tokens.popleft() + token = domain_seq.popleft() if token != 'domain': raise ParseError('domain keyword not found after define statement') - self.domain_name = domain_list.popleft() + return False + self.domain_name = domain_seq.popleft() return True def _parse_requirements(self, tokens: deque) -> bool: - self.requirements = list(tokens) - if ':strips' not in self.requirements: + self._requirements = list(tokens) + if ':strips' not in self._requirements: raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') return True def _parse_constants(self, tokens: deque) -> bool: - self.constants = parse_variables(tokens) + self.constants = parse_variables(tokens, self._types) return True + # noinspection PyUnusedLocal def _parse_types(self, tokens: deque) -> bool: - self.types = True + self._types = True return True def _parse_predicates(self, tokens: deque) -> bool: while tokens: predicate = tokens.popleft() pred_name = predicate.popleft() - new_predicate = [pred_name] + parse_variables(predicate, self.types) + new_predicate = [pred_name] + parse_variables(predicate, self._types) self.predicates.append(new_predicate) return True def _parse_action(self, tokens) -> bool: - self.action_name = tokens.pop() - self.parameters = [] - self.preconditions = [] - self.effects = [] + self._action_name = tokens.popleft() match = {':parameters': self._parse_parameters, - ':precondition': self._parse_precondition, - ':effect': self._parse_effect + ':precondition': self._parse_preconditions, + ':effect': self._parse_effects } parse_tokens(match, tokens) - params = [p[0] for p in self.parameters] - action_str = build_expr_string(self.action_name, params) - action = PlanningAction(expr(action_str), self.preconditions, self.effects) + params = [p[0] 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: - param_list = tokens.popleft() - self.parameters = parse_variables(param_list, self.types) + if tokens: + param_list = tokens.popleft() + self._parameters = parse_variables(param_list, self._types) return True - def _parse_precondition(self, tokens: deque) -> bool: - precond_list = tokens.popleft() - self.preconditions = parse_formula(precond_list) + def _parse_preconditions(self, tokens: deque) -> bool: + if tokens: + precond_seq = tokens.popleft() + self._preconditions = parse_formula(precond_seq) return True - def _parse_effect(self, tokens: deque) -> bool: - effects_list = tokens.popleft() - self.effects = parse_formula(effects_list) + def _parse_effects(self, tokens: deque) -> bool: + if tokens: + effects_seq = tokens.popleft() + self._effects = parse_formula(effects_seq) return True def read(self, filename) -> None: - pddl_list = read_pddl_file(filename) + pddl = read_pddl_file(filename) # Use dictionaries for parsing. If the token matches the key, then call the associated value (method) # for parsing. @@ -235,16 +262,15 @@ def read(self, filename) -> None: ':action': self._parse_action } - parse_tokens(match, pddl_list) + parse_tokens(match, pddl) -class PDDLProblemParser: - def __init__(self, types): +class ProblemParser: + def __init__(self): self.problem_name = '' - self.types = types - self.objects = [] - self.init = [] - self.goal = [] + self.domain_name = '' + self.initial_state = [] + self.goals = [] def _parse_define(self, tokens: deque) -> bool: problem_list = tokens.popleft() @@ -258,17 +284,17 @@ def _parse_domain(self, tokens: deque) -> bool: self.domain_name = tokens.popleft() return True - def _parse_init(self, tokens: deque): - self.initial_kb = _parse_expr_list(tokens) + def _parse_init(self, tokens: deque) -> bool: + self.initial_state = _parse_expr_list(tokens) return True - def _parse_goal(self, tokens: deque): + def _parse_goal(self, tokens: deque) -> bool: goal_list = tokens.popleft() - self.goal = parse_formula(goal_list) + self.goals = parse_formula(goal_list) return True - def read(self, filename): - pddl_list = read_pddl_file(filename) + def read(self, filename) -> None: + pddl = read_pddl_file(filename) # Use dictionaries for parsing. If the token matches the key, then call the associated value (method) # for parsing. @@ -278,4 +304,4 @@ def read(self, filename): ':goal': self._parse_goal } - parse_tokens(match, pddl_list) + parse_tokens(match, pddl) From 6344a9f9c2871514066fd2f54b1c6d7392b394b7 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Fri, 11 May 2018 22:19:45 -0400 Subject: [PATCH 17/40] Last version of PDDL parsing that attempts to save tokens and their line/col info for error reporting. Didn't pan out because of carrying the line/col info in a separate data structure that makes it hard to keep in sync, plus it just seems like overkill. --- pddl_parse.py | 457 ++++++++++++++++++++++++++++++++------------------ planning.py | 80 +++++++-- 2 files changed, 364 insertions(+), 173 deletions(-) diff --git a/pddl_parse.py b/pddl_parse.py index 19fcaf97b..0016adac9 100644 --- a/pddl_parse.py +++ b/pddl_parse.py @@ -1,5 +1,4 @@ -import os -from typing import Deque +from typing import Deque, AnyStr from collections import deque CHAR = 0 @@ -12,161 +11,331 @@ class ParseError(Exception): pass -def read_pddl_file(filename) -> deque: +class Tokens: + def __init__(self, token_deque, info_deque): + self.token_deque = token_deque + self.info_deque = info_deque + self.last_charstr = '' + self.last_line = 0 + self.last_col = 0 + + def __repr__(self): + return str(self.token_deque) + + def popleft(self): + try: + token = self.token_deque.popleft() + charstr, line, col = self.info_deque.popleft() + if type(token) is Deque: + open_paren = 1 + while open_paren != 0: + if charstr == '(': + open_paren += 1 + elif charstr == ')': + open_paren -= 1 + charstr, line, col = self.info_deque.popleft() + self.last_charstr, self.last_line, self.last_col = charstr, line, col + except IndexError: + exc_text = "EOF encountered. Last token processed was '{}' on line {}, col {}." + raise ParseError(exc_text.format(self.last_charstr, self.last_line, self.last_col)) + return token + + def pop(self): + try: + token = self.token_deque.pop() + charstr, line, col = self.info_deque.pop() + self.last_charstr, self.last_line, self.last_col = charstr, line, col + except IndexError: + raise ParseError("EOF encountered. Last token processed was '{}' " + + "on line {}, col {}.".format(self.last_charstr, self.last_line, self.last_col)) + return token + + def lookahead(self, idx=0): + try: + return self.token_deque[idx] + except IndexError: + return None + + +def read_pddl_file(filename) -> Tokens: with open(filename) as f: # read in lines from PDDL file and remove newline characters - lines = [line.strip() for line in f.readlines()] - strip_comments(lines) + lines = deque([line.strip() for line in f.readlines()]) + strip_comments_and_blank_lines(lines) # transform into Python-compatible S-expressions (using deques of strings) - return readlines(filename, lines) + tokens, info_deque = tokenize(lines) + token_deque = read_from_tokens(tokens) + return Tokens(token_deque, info_deque) -def strip_comments(lines) -> None: +def strip_comments_and_blank_lines(lines: deque) -> 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 readlines(filename: str, pddl: list) -> deque: - """Read PDDL contained in a string.""" - return parse(filename, tokenize(pddl)) + # remove any blank lines + for i in range(len(lines)-1, -1, -1): + if lines[i] == '': + del lines[i] + + +def tokenize(lines: deque): + """Tokenize PDDL contained in a string. + Add line number and column number info for error reporting.""" + if not lines: + raise ParseError('No lines in file') + # join all lines into a single line of PDDL + pddl = ''.join(lines) + tokens = deque(pddl.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split()) + token_info = deque() + + # scan lines in file and record placement of each token + line = lines.popleft() + line_idx = 0 + curr_col_idx = 0 + for idx, t in enumerate(tokens): + if not line: + raise ParseError("Couldn't find token {}".format(t)) + while True: + col_idx = line.find(t, curr_col_idx) + if col_idx == -1: + curr_col_idx = 0 + if not lines: + raise ParseError("Couldn't find token {}".format(t)) + line = lines.popleft() + line_idx += 1 + continue + else: + # actual line and col numbers are line_idx+1 and col_idx+1 + token_info.append((t, line_idx+1, col_idx+1)) + curr_col_idx = col_idx + 1 + break + return tokens, token_info -def parse(filename: str, tokens: deque): - # read the tokens one at time (left to right) and separate them into list of keywords and parameters. +def read_from_tokens(tokens: deque): + """Read an expression from a sequence of tokens.""" if len(tokens) == 0: - raise ParseError('unexpected EOF while reading {}.'.format(os.path.basename(filename))) - char, line_no, col_no = tokens.popleft() - if '(' == char: + raise ParseError('unexpected EOF while reading') + token = tokens.popleft() + if '(' == token: D = deque() - s = '' - while True: - try: - if tokens[0][CHAR] == '(': - D.append(parse(filename, tokens)) - elif tokens[0][CHAR] == ')': - if s: - D.append(s) - tokens.popleft() - break - elif tokens[0][CHAR] in WHITESPACE: - if s: - D.append(s) - tokens.popleft() - s = '' - else: - char, line_no, col_no = tokens.popleft() - s += char - except IndexError: - raise ParseError('unexpected EOF while reading {}.'.format(os.path.basename(filename))) - return D - elif ')' == char: - raise ParseError("unexpected ')' token in {}, line {}, col {}".format(os.path.basename(filename), - line_no + 1, col_no + 1)) + try: + while tokens[0] != ')': + D.append(read_from_tokens(tokens)) + tokens.popleft() # pop off ')' + return D + except IndexError: + raise ParseError('unexpected EOF while parsing {}'.format(list(D))) + elif ')' == token: + raise ParseError('unexpected ")" token') else: - return char + return token -def tokenize(pddl: list) -> deque: - """Convert a string into a deque of tokens.""" - d = deque() - for lineno, line in enumerate(pddl): - for column, char in enumerate(line): - d.append((char, lineno, column)) - return d +def parse_tokens(parsers, tokens): + while tokens: + for parser in parsers: + if parser.detect(tokens): + parser.parse(tokens) + break + else: + # remove a token only when none of the parsers are successful + tokens.popleft() -def parse_tokens(match_dict, token_list): - def match_tokens(tokens): - if not tokens: - return False - item = tokens.popleft() - if isinstance(item, Deque): - match_tokens(item) - else: - item = item.lower() - for text in match_dict: - if item.startswith(text): - if match_dict[text](tokens): - break - return True +class Sequence: + def __init__(self): + pass - while True: - if not match_tokens(token_list): - break + def parse(self, tokens: deque): + token = tokens.popleft() + if type(token) is not Deque: + raise ParseError('Expected sequence, but found "{}" instead.'.format(token)) -def parse_variables(tokens, has_types) -> list: - """ Extracts a list of variables from the PDDL. """ - variables = [] - while tokens: +class Define: + def __init__(self): + pass + + def detect(self, tokens): + try: + return tokens.lookahead() == 'define' + except IndexError: + return False + + def parse(self, tokens): token = tokens.popleft() - if not token.startswith('?'): - raise ParseError("Unrecognized variable name ({0}) " + - "that doesn't begin with a question mark".format(token)) - pred_var = token[1:] - if has_types: + if token != 'define': + raise ParseError('Expected "define" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) + return token + + +class DefineProblem(Define): + def __init__(self): + super().__init__() + self.problem_name = None + + def detect(self, tokens): + if not super().detect(tokens): + return False + try: + return tokens.lookahead(1)[0] == 'problem' and type(tokens.lookahead(1)[1]) is str + except IndexError: + return False + except TypeError: + return False + + def parse(self, tokens): + super().parse(tokens) + problem_seq = tokens.popleft() + token = problem_seq.popleft() + if token != 'problem': + raise ParseError('Expected "problem" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) + self.problem_name = problem_seq.popleft() + + +class DefineDomain(Define): + def __init__(self): + super().__init__() + self.domain_name = None + + def detect(self, tokens): + if not super().detect(tokens): + return False + try: + return tokens.lookahead(1)[0] == 'domain' and type(tokens.lookahead(1)[1]) is str + except IndexError: + return False + except TypeError: + return False + + def parse(self, tokens): + if self.domain_name: + raise ParseError("Domain line occurs twice in domain file.") + super().parse(tokens) + domain_seq = tokens.popleft() + token = domain_seq.popleft() + if token != 'domain': + raise ParseError('Expected "domain" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) + self.domain_name = domain_seq.popleft() + + +class Requirements: + def __init__(self): + self.requirements = [] + + def detect(self, text): + try: + token = text.lookahead() + if token: + return token[0].startswith(':requirements') + else: + return False + except IndexError: + return False + + def parse(self, tokens): + if self.requirements: + raise ParseError("Requirements line occurs twice in domain file.") + token_list = tokens.popleft() + token_list.popleft() + while token_list: + self.requirements.append(token_list.popleft()) + if ':strips' not in self.requirements: + raise ParseError(':strips is not in list of domain requirements on line {}.'.format(tokens.last_line)) + + +class Variables: + @classmethod + def parse(cls, tokens): + """ Extracts a list of variables from the PDDL. """ + variables = [] + while tokens: + token = tokens.popleft() + if not token.startswith('?'): + raise ParseError("Unrecognized variable name ({0}) " + + "that doesn't begin with a question mark".format(token)) + try: + pred_var = token[1:] + except IndexError: + raise ParseError("Variable name format incorrect") # lookahead to see if there's a dash indicating an upcoming type name - if tokens[0] == '-': + if tokens.lookahead() == '-': # get rid of the dash character and the type name tokens.popleft() tokens.popleft() - variables.append(pred_var) - return variables - - -def _parse_single_expr_string(tokens: deque) -> str: - 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().capitalize() - variables = [] - while tokens: - param = tokens.popleft() - if param.startswith('?'): - variables.append(param[1:].lower()) - else: - variables.append(param) - return build_expr_string(expr_name, variables) + variables.append(pred_var) + return variables -def _parse_expr_list(tokens) -> list: - expr_lst = [] - while tokens: - token = tokens.popleft() - expr_lst.append(_parse_single_expr_string(token)) - return expr_lst +class Predicate: + def __init__(self): + self.expr = None + def detect(self, tokens): + return True -def parse_formula(tokens: deque) -> list: - expr_lst = [] - token = tokens.popleft() - if token.lower() == 'and': # preconds and effects only use 'and' keyword - exprs = _parse_expr_list(tokens) - expr_lst.extend(exprs) - else: # parse single expression - expr_lst.append(_parse_single_expr_string(deque([token]) + tokens)) - return expr_lst - - -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 - 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(self, tokens): + if tokens.lookahead() == 'not': + # expression is not(e), so next, parse the expression e before prepending the ~ operator to it. + token = tokens.pop() + e = cls.parse(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().capitalize() + variables = [] + while tokens: + param = tokens.popleft() + if param.startswith('?'): + variables.append(param[1:].lower()) + else: + variables.append(param) + self._build_expr_string(expr_name, variables) + return True + + def _build_expr_string(self, expr_name: str, variables: list) -> str: + # can't have actions with a dash in the name; it confuses the Expr class + estr = expr_name.replace('-', '').capitalize() + '(' + vlen = len(variables) + if vlen: + for i in range(vlen - 1): + estr += variables[i] + ', ' + estr += variables[vlen - 1] + estr += ')' + self.expr = expr(estr) + + +class PredicateList: + def __init__(self): + pass + + def parse(self, tokens): + expr_lst = [] + while tokens: + token = tokens.popleft() + expr_lst.append(_parse_single_expr_string(token)) + return expr_lst + + +class Formula: + def __init__(self): + pass + + def parse(self, tokens): + expr_lst = [] + token = tokens.popleft() + if token.lower() == 'and': # preconds and effects only use 'and' keyword + exprs = _parse_expr_list(tokens) + expr_lst.extend(exprs) + else: # parse single expression + expr_lst.append(_parse_single_expr_string(deque([token]) + tokens)) + return expr_lst class DomainParser: @@ -182,21 +351,6 @@ def __init__(self): self._preconditions = [] self._effects = [] - def _parse_define(self, tokens: deque) -> bool: - domain_seq = tokens.popleft() - token = domain_seq.popleft() - if token != 'domain': - raise ParseError('domain keyword not found after define statement') - return False - self.domain_name = domain_seq.popleft() - return True - - def _parse_requirements(self, tokens: deque) -> bool: - self._requirements = list(tokens) - if ':strips' not in self._requirements: - raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') - return True - def _parse_constants(self, tokens: deque) -> bool: self.constants = parse_variables(tokens, self._types) return True @@ -251,18 +405,8 @@ def _parse_effects(self, tokens: deque) -> bool: def read(self, filename) -> None: pddl = read_pddl_file(filename) - - # 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) + parsers = [DefineDomain(), Requirements()] + parse_tokens(parsers, pddl) class ProblemParser: @@ -295,13 +439,6 @@ def _parse_goal(self, tokens: deque) -> bool: def read(self, filename) -> None: pddl = read_pddl_file(filename) + parsers = [DefineProblem()] + parse_tokens(parsers, pddl) - # 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) diff --git a/planning.py b/planning.py index 309d0bab7..707feda2b 100644 --- a/planning.py +++ b/planning.py @@ -1,5 +1,6 @@ """Planning (Chapters 10-11) """ +import sys import os from logic import fol_bc_and from utils import expr, Expr, partition @@ -117,7 +118,13 @@ def __init__(self, expression, preconds, effects): self.args = expression.args self.subst = None self.preconds = preconds + 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 self.effects = effects + 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 def __repr__(self): return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, *self.args), @@ -130,7 +137,11 @@ def copy(self): act.args = self.args[:] act.subst = self.subst act.preconds = self.preconds.copy() + act.precond_pos = self.precond_pos.copy() + act.precond_neg = self.precond_neg.copy() act.effects = self.effects.copy() + act.effect_add = self.effect_add.copy() + act.effect_rem = self.effect_rem.copy() return act def substitute(self, subst, e): @@ -161,17 +172,15 @@ def check_pos_precond(self, kb, precond, subst): def check_precond(self, kb): """Checks if preconditions are satisfied in the current state""" - precond_neg, precond_pos = partition(self.preconds, is_negative_clause) - precond_neg = set(e.args[0] for e in precond_neg) # change the negative Exprs to positive - yield from self.check_neg_precond(kb, precond_neg, self.check_pos_precond(kb, precond_pos, {})) + yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) def act(self, subst, kb): """ Executes the action on a new copy of the PlanningKB """ new_kb = PlanningKB(kb.goal_clauses, kb.clause_set) - neg_effects, pos_effects = partition(self.effects, is_negative_clause) - neg_effects = set(self.substitute(subst, e.args[0]) for e in neg_effects) - pos_effects = set(self.substitute(subst, e) for e in pos_effects) - new_kb.clause_set = frozenset(kb.clause_set - neg_effects | pos_effects) + 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 @@ -201,7 +210,7 @@ def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: def gather_test_pairs() -> list: - pddl_direntries = os.scandir(os.getcwd() + os.sep + 'pddl_files') + pddl_direntries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('.pddl')] domain_objects = [] problem_objects = [] for de in pddl_direntries: @@ -209,20 +218,62 @@ def gather_test_pairs() -> list: domain_parser = DomainParser() domain_parser.read(de.path) domain_objects.append(domain_parser) - except ParseError: + except ParseError as pe1: try: problem_parser = ProblemParser() problem_parser.read(de.path) problem_objects.append(problem_parser) - except ParseError: - raise ParseError('Unparseable PDDL file: {}'.format(de.name)) + except ParseError as pe2: + exc_text = "Unable to recognize format of {}\n".format(de.name) + exc_text += pe1.args[0] + '\n' + exc_text += pe2.args[0] + '\n' + raise ParseError(exc_text) object_pairs = [] for p in problem_objects: for d in domain_objects: if p.domain_name == d.domain_name: object_pairs.append((d, p)) - return object_pairs + if object_pairs: + return object_pairs + else: + raise ParseError('No matching PDDL domain and problem files found.') + + +def air_cargo(): + goals = [expr('At(C1, JFK)'), expr('At(C2, SFO)')] + + init = PlanningKB(goals, + [expr('At(C1, SFO)'), + expr('At(C2, JFK)'), + expr('At(P1, SFO)'), + expr('At(P2, JFK)'), + expr('Cargo(C1)'), + expr('Cargo(C2)'), + expr('Plane(P1)'), + expr('Plane(P2)'), + expr('Airport(JFK)'), + expr('Airport(SFO)')]) + + # Actions + # Load + precond = [expr('At(c, a)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] + effect = [expr('In(c, p)'), expr('~At(c, a)')] + load = PlanningAction(expr('Load(c, p, a)'), precond, effect) + + # Unload + precond = [expr('In(c, p)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] + effect = [expr('At(c, a)'), expr('~In(c, p)')] + unload = PlanningAction(expr('Unload(c, p, a)'), precond, effect) + + # Fly + # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function + precond = [expr('At(p, f)'), expr('Plane(p)'), expr('Airport(f)'), expr('Airport(to)')] + effect = [expr('At(p, to)'), expr('~At(p, f)')] + fly = PlanningAction(expr('Fly(p, f, to)'), precond, effect) + + p = PlanningProblem(init, [load, unload, fly]) + print_solution(astar_search(p)) def spare_tire(): @@ -317,4 +368,7 @@ def test_solutions(): if __name__ == '__main__': - test_solutions() + air_cargo() + spare_tire() + sussman_anomaly() + put_on_shoes() From 6be6d6ef8ca0c50a46ed8d8305de4d1c1fb39172 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 13 May 2018 17:41:10 -0400 Subject: [PATCH 18/40] Spare tire PDDL now working --- pddl_files/spare-tire-domain.pddl | 37 ++++++++++++++++++++++++++++++ pddl_files/spare-tire-problem.pddl | 9 ++++++++ 2 files changed, 46 insertions(+) create mode 100755 pddl_files/spare-tire-domain.pddl create mode 100755 pddl_files/spare-tire-problem.pddl 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)) +) From 3c93c291256e77499fc57d16d396c5dd66254e00 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 13 May 2018 17:41:43 -0400 Subject: [PATCH 19/40] Aircargo and Blocks PDDL files now working. --- blocks-domain.pddl | 49 -------------------------------- blocks-problem.pddl | 18 ------------ pddl_files/aircargo-domain.pddl | 31 ++++++++++++++++++++ pddl_files/aircargo-problem.pddl | 17 +++++++++++ pddl_files/blocks-domain.pddl | 17 +++++++++-- pddl_files/blocks-problem.pddl | 12 ++++---- 6 files changed, 67 insertions(+), 77 deletions(-) delete mode 100755 blocks-domain.pddl delete mode 100755 blocks-problem.pddl create mode 100755 pddl_files/aircargo-domain.pddl create mode 100755 pddl_files/aircargo-problem.pddl diff --git a/blocks-domain.pddl b/blocks-domain.pddl deleted file mode 100755 index c553461b4..000000000 --- a/blocks-domain.pddl +++ /dev/null @@ -1,49 +0,0 @@ -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; 4 Op-blocks world -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(define (domain BLOCKS) - (:requirements :strips :typing) - (:types block) - (:predicates (on ?x - block ?y - block) - (ontable ?x - block) - (clear ?x - block) - (handempty) - (holding ?x - block) - ) - - (:action pick-up - :parameters (?x - block) - :precondition (and (clear ?x) (ontable ?x) (handempty)) - :effect - (and (not (ontable ?x)) - (not (clear ?x)) - (not (handempty)) - (holding ?x))) - - (:action put-down - :parameters (?x - block) - :precondition (holding ?x) - :effect - (and (not (holding ?x)) - (clear ?x) - (handempty) - (ontable ?x))) - (:action stack - :parameters (?x - block ?y - block) - :precondition (and (holding ?x) (clear ?y)) - :effect - (and (not (holding ?x)) - (not (clear ?y)) - (clear ?x) - (handempty) - (on ?x ?y))) - (:action unstack - :parameters (?x - block ?y - block) - :precondition (and (on ?x ?y) (clear ?x) (handempty)) - :effect - (and (holding ?x) - (clear ?y) - (not (clear ?x)) - (not (handempty)) - (not (on ?x ?y))))) diff --git a/blocks-problem.pddl b/blocks-problem.pddl deleted file mode 100755 index 76b5155c0..000000000 --- a/blocks-problem.pddl +++ /dev/null @@ -1,18 +0,0 @@ -(define (problem BLOCKS-4-0) - -(:domain BLOCKS) - -(:objects D B A C - block) - -(:INIT (CLEAR C) - (CLEAR A) - (CLEAR B) - (CLEAR D) - (ONTABLE C) - (ONTABLE A) - (ONTABLE B) - (ONTABLE D) - (HANDEMPTY)) - -(:goal (AND (ON D C) (ON C B) (ON B A))) -) 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 index c553461b4..3e52867eb 100755 --- a/pddl_files/blocks-domain.pddl +++ b/pddl_files/blocks-domain.pddl @@ -1,8 +1,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; 4 Op-blocks world +;;; Building block towers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(define (domain BLOCKS) +(define (domain BlocksWorld) (:requirements :strips :typing) (:types block) (:predicates (on ?x - block ?y - block) @@ -29,6 +29,7 @@ (clear ?x) (handempty) (ontable ?x))) + (:action stack :parameters (?x - block ?y - block) :precondition (and (holding ?x) (clear ?y)) @@ -38,6 +39,7 @@ (clear ?x) (handempty) (on ?x ?y))) + (:action unstack :parameters (?x - block ?y - block) :precondition (and (on ?x ?y) (clear ?x) (handempty)) @@ -46,4 +48,13 @@ (clear ?y) (not (clear ?x)) (not (handempty)) - (not (on ?x ?y))))) + (not (on ?x ?y)))) + + (:action pick-up + :parameters (?x - block) + :precondition (and (clear ?x) (ontable ?x) (handempty)) + :effect + (and (not (ontable ?x)) + (not (clear ?x)) + (not (handempty)) + (holding ?x)))) diff --git a/pddl_files/blocks-problem.pddl b/pddl_files/blocks-problem.pddl index 76b5155c0..489d62511 100755 --- a/pddl_files/blocks-problem.pddl +++ b/pddl_files/blocks-problem.pddl @@ -1,18 +1,16 @@ -(define (problem BLOCKS-4-0) +(define (problem ThreeBlockTower) -(:domain BLOCKS) +(:domain BlocksWorld) (:objects D B A C - block) (:INIT (CLEAR C) (CLEAR A) - (CLEAR B) - (CLEAR D) + (CLEAR B) (ONTABLE C) (ONTABLE A) - (ONTABLE B) - (ONTABLE D) + (ONTABLE B) (HANDEMPTY)) -(:goal (AND (ON D C) (ON C B) (ON B A))) +(:goal (AND (ON B C) (ON A B))) ) From c5fe84762444e71a0fb98471fba49c311262850f Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 13 May 2018 20:43:43 -0400 Subject: [PATCH 20/40] Revised BlocksWorld and added SussmanAnomaly --- pddl_files/blocks-domain.pddl | 68 ++++++------------------- pddl_files/blocks-problem.pddl | 25 ++++----- pddl_files/sussman-anomaly-problem.pddl | 15 ++++++ 3 files changed, 44 insertions(+), 64 deletions(-) create mode 100755 pddl_files/sussman-anomaly-problem.pddl diff --git a/pddl_files/blocks-domain.pddl b/pddl_files/blocks-domain.pddl index 3e52867eb..8dbcdaf9b 100755 --- a/pddl_files/blocks-domain.pddl +++ b/pddl_files/blocks-domain.pddl @@ -3,58 +3,22 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (domain BlocksWorld) - (:requirements :strips :typing) - (:types block) - (:predicates (on ?x - block ?y - block) - (ontable ?x - block) - (clear ?x - block) - (handempty) - (holding ?x - block) - ) + (:requirements :strips) + (:predicates (on ?x ?y) + (clear ?x) + (block ?x) + ) - (:action pick-up - :parameters (?x - block) - :precondition (and (clear ?x) (ontable ?x) (handempty)) - :effect - (and (not (ontable ?x)) - (not (clear ?x)) - (not (handempty)) - (holding ?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 put-down - :parameters (?x - block) - :precondition (holding ?x) - :effect - (and (not (holding ?x)) - (clear ?x) - (handempty) - (ontable ?x))) + (: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))) + ) +) - (:action stack - :parameters (?x - block ?y - block) - :precondition (and (holding ?x) (clear ?y)) - :effect - (and (not (holding ?x)) - (not (clear ?y)) - (clear ?x) - (handempty) - (on ?x ?y))) - - (:action unstack - :parameters (?x - block ?y - block) - :precondition (and (on ?x ?y) (clear ?x) (handempty)) - :effect - (and (holding ?x) - (clear ?y) - (not (clear ?x)) - (not (handempty)) - (not (on ?x ?y)))) - - (:action pick-up - :parameters (?x - block) - :precondition (and (clear ?x) (ontable ?x) (handempty)) - :effect - (and (not (ontable ?x)) - (not (clear ?x)) - (not (handempty)) - (holding ?x)))) diff --git a/pddl_files/blocks-problem.pddl b/pddl_files/blocks-problem.pddl index 489d62511..afda1a2b0 100755 --- a/pddl_files/blocks-problem.pddl +++ b/pddl_files/blocks-problem.pddl @@ -1,16 +1,17 @@ (define (problem ThreeBlockTower) -(:domain BlocksWorld) + (:domain BlocksWorld) -(:objects D B A C - block) + (:init + (on A Table) + (on B Table) + (on C Table) + (block A) + (block B) + (block C) + (clear A) + (clear B) + (clear C) + ) -(:INIT (CLEAR C) - (CLEAR A) - (CLEAR B) - (ONTABLE C) - (ONTABLE A) - (ONTABLE B) - (HANDEMPTY)) - -(:goal (AND (ON B C) (ON A B))) -) + (:goal (and (on A B) (on B C))) 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))) +) From 2644171c40d531fe701cc575f0ab8534e51d3f5a Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 13 May 2018 22:06:58 -0400 Subject: [PATCH 21/40] Revised bug in blocks problem. Added shoes domain and problem. --- pddl_files/blocks-problem.pddl | 1 + pddl_files/shoes-domain.pddl | 37 ++++++++++++++++++++++++++++++++++ pddl_files/shoes-problem.pddl | 12 +++++++++++ 3 files changed, 50 insertions(+) create mode 100755 pddl_files/shoes-domain.pddl create mode 100755 pddl_files/shoes-problem.pddl diff --git a/pddl_files/blocks-problem.pddl b/pddl_files/blocks-problem.pddl index afda1a2b0..c0227056b 100755 --- a/pddl_files/blocks-problem.pddl +++ b/pddl_files/blocks-problem.pddl @@ -15,3 +15,4 @@ ) (:goal (and (on A B) (on B C))) +) diff --git a/pddl_files/shoes-domain.pddl b/pddl_files/shoes-domain.pddl new file mode 100755 index 000000000..183759603 --- /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 Right_Shoe_On + :parameters () + :precondition (and (on RightSock ?x) (rightfoot ?x) (not (on RightShoe ?x))) + :effect (and (on RightShoe ?x)) + ) + + (:action Right_Sock_On + :parameters () + :precondition (and (clear ?x) (rightfoot ?x)) + :effect (and (on RightSock ?x) (not (clear ?x))) + ) + + (:action Left_Shoe_On + :parameters () + :precondition (and (on LeftSock ?x) (leftfoot ?x) (not (on LeftShoe ?x))) + :effect (and (on LeftShoe ?x)) + ) + + (:action Left_Sock_On + :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))) +) From 17607ff67eaf8cea245c427c317cafb41208bfa2 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 13 May 2018 22:56:16 -0400 Subject: [PATCH 22/40] Initial TPP domain and problem files --- pddl_files/tpp-domain.pddl | 65 +++++++++++++++++++++++++++++++++++++ pddl_files/tpp-problem.pddl | 38 ++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 pddl_files/tpp-domain.pddl create mode 100644 pddl_files/tpp-problem.pddl 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..fc804b67a --- /dev/null +++ b/pddl_files/tpp-problem.pddl @@ -0,0 +1,38 @@ +(define (problem TPP) +(:domain TPP-Propositional) +(:objects + Goods1 Goods2 Goods3 Goods4 - 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) + (ready-to-load Goods3 Market1 Level0) + (ready-to-load Goods4 Market1 Level0) + (stored Goods1 Level0) + (stored Goods2 Level0) + (stored Goods3 Level0) + (stored Goods4 Level0) + (loaded Goods1 Truck1 Level0) + (loaded Goods2 Truck1 Level0) + (loaded Goods3 Truck1 Level0) + (loaded Goods4 Truck1 Level0) + (connected Depot1 Market1) + (connected Market1 Depot1) + (on-sale Goods1 Market1 Level1) + (on-sale Goods2 Market1 Level1) + (on-sale Goods3 Market1 Level1) + (on-sale Goods4 Market1 Level1) + (at Truck1 Depot1)) + +(:goal (and + (stored Goods1 Level1) + (stored Goods2 Level1) + (stored Goods3 Level1) + (stored Goods4 Level1))) + +) \ No newline at end of file From eab18fd61d1696db97648e7b893a9a25153e5e19 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 14 May 2018 08:41:33 -0400 Subject: [PATCH 23/40] TPP task 02 problem, duplicated from Pyperplan tests --- pddl_files/tpp-problem.pddl | 52 ++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/pddl_files/tpp-problem.pddl b/pddl_files/tpp-problem.pddl index fc804b67a..b98d9188d 100644 --- a/pddl_files/tpp-problem.pddl +++ b/pddl_files/tpp-problem.pddl @@ -1,38 +1,30 @@ +;; TPP Task 02 + (define (problem TPP) (:domain TPP-Propositional) (:objects - Goods1 Goods2 Goods3 Goods4 - goods - Truck1 - truck - Market1 - market - Depot1 - depot - Level0 Level1 - level) + 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) - (ready-to-load Goods3 Market1 Level0) - (ready-to-load Goods4 Market1 Level0) - (stored Goods1 Level0) - (stored Goods2 Level0) - (stored Goods3 Level0) - (stored Goods4 Level0) - (loaded Goods1 Truck1 Level0) - (loaded Goods2 Truck1 Level0) - (loaded Goods3 Truck1 Level0) - (loaded Goods4 Truck1 Level0) - (connected Depot1 Market1) - (connected Market1 Depot1) - (on-sale Goods1 Market1 Level1) - (on-sale Goods2 Market1 Level1) - (on-sale Goods3 Market1 Level1) - (on-sale Goods4 Market1 Level1) - (at Truck1 Depot1)) + (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) - (stored Goods3 Level1) - (stored Goods4 Level1))) + (stored goods1 level1) + (stored goods2 level1))) -) \ No newline at end of file +) From 3dbdc6fd4b281cf08233de3b7b48e6c0249b2582 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 14 May 2018 08:43:25 -0400 Subject: [PATCH 24/40] Planning now working with PDDL parsing tests. --- pddl_parse.py | 514 ++++++++++++++++++++------------------------------ planning.py | 155 ++------------- 2 files changed, 216 insertions(+), 453 deletions(-) diff --git a/pddl_parse.py b/pddl_parse.py index 0016adac9..d56df3c43 100644 --- a/pddl_parse.py +++ b/pddl_parse.py @@ -1,345 +1,161 @@ -from typing import Deque, AnyStr +import os +from typing import Deque from collections import deque -CHAR = 0 -WHITESPACE = [' ', '\t'] -Symbol = str # A Lisp Symbol is implemented as a Python str -List = list # A Lisp List is implemented as a Python list - class ParseError(Exception): pass -class Tokens: - def __init__(self, token_deque, info_deque): - self.token_deque = token_deque - self.info_deque = info_deque - self.last_charstr = '' - self.last_line = 0 - self.last_col = 0 - - def __repr__(self): - return str(self.token_deque) - - def popleft(self): - try: - token = self.token_deque.popleft() - charstr, line, col = self.info_deque.popleft() - if type(token) is Deque: - open_paren = 1 - while open_paren != 0: - if charstr == '(': - open_paren += 1 - elif charstr == ')': - open_paren -= 1 - charstr, line, col = self.info_deque.popleft() - self.last_charstr, self.last_line, self.last_col = charstr, line, col - except IndexError: - exc_text = "EOF encountered. Last token processed was '{}' on line {}, col {}." - raise ParseError(exc_text.format(self.last_charstr, self.last_line, self.last_col)) - return token - - def pop(self): - try: - token = self.token_deque.pop() - charstr, line, col = self.info_deque.pop() - self.last_charstr, self.last_line, self.last_col = charstr, line, col - except IndexError: - raise ParseError("EOF encountered. Last token processed was '{}' " + - "on line {}, col {}.".format(self.last_charstr, self.last_line, self.last_col)) - return token - - def lookahead(self, idx=0): - try: - return self.token_deque[idx] - except IndexError: - return None - - -def read_pddl_file(filename) -> Tokens: +def read_pddl_file(filename) -> list: with open(filename) as f: # read in lines from PDDL file and remove newline characters - lines = deque([line.strip() for line in f.readlines()]) - strip_comments_and_blank_lines(lines) + lines = [line.strip() for line in f.readlines()] + strip_comments(lines) + # join all lines into single string + s = ''.join(lines) + # transform into Python-compatible S-expressions (using lists of strings) + return parse(s, os.path.basename(filename)) - # transform into Python-compatible S-expressions (using deques of strings) - tokens, info_deque = tokenize(lines) - token_deque = read_from_tokens(tokens) - return Tokens(token_deque, info_deque) - -def strip_comments_and_blank_lines(lines: deque) -> None: +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] - # remove any blank lines - for i in range(len(lines)-1, -1, -1): - if lines[i] == '': - del lines[i] - - -def tokenize(lines: deque): - """Tokenize PDDL contained in a string. - Add line number and column number info for error reporting.""" - if not lines: - raise ParseError('No lines in file') - # join all lines into a single line of PDDL - pddl = ''.join(lines) - tokens = deque(pddl.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split()) - token_info = deque() - - # scan lines in file and record placement of each token - line = lines.popleft() - line_idx = 0 - curr_col_idx = 0 - for idx, t in enumerate(tokens): - if not line: - raise ParseError("Couldn't find token {}".format(t)) - while True: - col_idx = line.find(t, curr_col_idx) - if col_idx == -1: - curr_col_idx = 0 - if not lines: - raise ParseError("Couldn't find token {}".format(t)) - line = lines.popleft() - line_idx += 1 - continue - else: - # actual line and col numbers are line_idx+1 and col_idx+1 - token_info.append((t, line_idx+1, col_idx+1)) - curr_col_idx = col_idx + 1 - break - return tokens, token_info +def parse(pddl, filename): + """Read PDDL contained in a string.""" + return read_from_tokens(tokenize(pddl), filename) + + +def tokenize(s: str) -> deque: + """Convert a string into a list of tokens.""" + return deque(s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split()) -def read_from_tokens(tokens: deque): + +def read_from_tokens(tokens: deque, filename: str): """Read an expression from a sequence of tokens.""" if len(tokens) == 0: - raise ParseError('unexpected EOF while reading') + raise ParseError('unexpected EOF while reading {}'.format(filename)) token = tokens.popleft() if '(' == token: D = deque() try: while tokens[0] != ')': - D.append(read_from_tokens(tokens)) + D.append(read_from_tokens(tokens, filename)) tokens.popleft() # pop off ')' return D except IndexError: - raise ParseError('unexpected EOF while parsing {}'.format(list(D))) + raise ParseError('unexpected EOF while reading {}'.format(filename)) elif ')' == token: - raise ParseError('unexpected ")" token') + raise ParseError('unexpected ) in {}'.format(filename)) else: return token -def parse_tokens(parsers, tokens): - while tokens: - for parser in parsers: - if parser.detect(tokens): - parser.parse(tokens) - break +def parse_tokens(match_dict, tokens): + def match_tokens(tokens): + if not isinstance(tokens, Deque): + return False + item = tokens.popleft() + if isinstance(item, deque): + match_tokens(item) else: - # remove a token only when none of the parsers are successful - tokens.popleft() + item = item.lower() + for text in match_dict: + if item.startswith(text): + if match_dict[text](tokens): + break + return True + while tokens: + if not match_tokens(tokens): + break -class Sequence: - def __init__(self): - pass - def parse(self, tokens: deque): - token = tokens.popleft() - if type(token) is not Deque: - raise ParseError('Expected sequence, but found "{}" instead.'.format(token)) +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 + 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 -class Define: - def __init__(self): - pass - - def detect(self, tokens): - try: - return tokens.lookahead() == 'define' - except IndexError: - return False - - def parse(self, tokens): +def _parse_variables(tokens, has_types) -> list: + """ Extracts a list of variables from the PDDL. """ + variables = [] + while tokens: token = tokens.popleft() - if token != 'define': - raise ParseError('Expected "define" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) - return token - - -class DefineProblem(Define): - def __init__(self): - super().__init__() - self.problem_name = None - - def detect(self, tokens): - if not super().detect(tokens): - return False - try: - return tokens.lookahead(1)[0] == 'problem' and type(tokens.lookahead(1)[1]) is str - except IndexError: - return False - except TypeError: - return False - - def parse(self, tokens): - super().parse(tokens) - problem_seq = tokens.popleft() - token = problem_seq.popleft() - if token != 'problem': - raise ParseError('Expected "problem" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) - self.problem_name = problem_seq.popleft() - - -class DefineDomain(Define): - def __init__(self): - super().__init__() - self.domain_name = None - - def detect(self, tokens): - if not super().detect(tokens): - return False - try: - return tokens.lookahead(1)[0] == 'domain' and type(tokens.lookahead(1)[1]) is str - except IndexError: - return False - except TypeError: - return False - - def parse(self, tokens): - if self.domain_name: - raise ParseError("Domain line occurs twice in domain file.") - super().parse(tokens) - domain_seq = tokens.popleft() - token = domain_seq.popleft() - if token != 'domain': - raise ParseError('Expected "domain" keyword at line {}, col {}'.format(tokens.last_line, tokens.last_col)) - self.domain_name = domain_seq.popleft() - - -class Requirements: - def __init__(self): - self.requirements = [] - - def detect(self, text): - try: - token = text.lookahead() - if token: - return token[0].startswith(':requirements') - else: - return False - except IndexError: - return False + if token.startswith('?'): + pred_var = token[1:] + else: + pred_var = token - def parse(self, tokens): - if self.requirements: - raise ParseError("Requirements line occurs twice in domain file.") - token_list = tokens.popleft() - token_list.popleft() - while token_list: - self.requirements.append(token_list.popleft()) - if ':strips' not in self.requirements: - raise ParseError(':strips is not in list of domain requirements on line {}.'.format(tokens.last_line)) - - -class Variables: - @classmethod - def parse(cls, tokens): - """ Extracts a list of variables from the PDDL. """ - variables = [] - while tokens: - token = tokens.popleft() - if not token.startswith('?'): - raise ParseError("Unrecognized variable name ({0}) " + - "that doesn't begin with a question mark".format(token)) - try: - pred_var = token[1:] - except IndexError: - raise ParseError("Variable name format incorrect") + if has_types: # lookahead to see if there's a dash indicating an upcoming type name - if tokens.lookahead() == '-': + if tokens[0] == '-': # get rid of the dash character and the type name tokens.popleft() tokens.popleft() - variables.append(pred_var) - return variables - - -class Predicate: - def __init__(self): - self.expr = None - - def detect(self, tokens): - return True - - def parse(self, tokens): - if tokens.lookahead() == 'not': - # expression is not(e), so next, parse the expression e before prepending the ~ operator to it. - token = tokens.pop() - e = cls.parse(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().capitalize() - variables = [] - while tokens: - param = tokens.popleft() - if param.startswith('?'): - variables.append(param[1:].lower()) - else: - variables.append(param) - self._build_expr_string(expr_name, variables) - return True - - def _build_expr_string(self, expr_name: str, variables: list) -> str: - # can't have actions with a dash in the name; it confuses the Expr class - estr = expr_name.replace('-', '').capitalize() + '(' - vlen = len(variables) - if vlen: - for i in range(vlen - 1): - estr += variables[i] + ', ' - estr += variables[vlen - 1] - estr += ')' - self.expr = expr(estr) - - -class PredicateList: - def __init__(self): - pass - - def parse(self, tokens): - expr_lst = [] + variables.append(pred_var) + return variables + + +def _parse_single_expr_string(tokens: deque) -> str: + if not isinstance(tokens, Deque): + 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().lower() + variables = [] while tokens: - token = tokens.popleft() - expr_lst.append(_parse_single_expr_string(token)) - return expr_lst - + param = tokens.popleft() + if param.startswith('?'): + variables.append(param[1:].lower()) + else: + variables.append(param.capitalize()) + return _build_expr_string(expr_name, variables) -class Formula: - def __init__(self): - pass - def parse(self, tokens): - expr_lst = [] +def _parse_expr_list(tokens) -> list: + if not isinstance(tokens, Deque): + raise ParseError('Expected expression list') + expr_lst = [] + while tokens: token = tokens.popleft() - if token.lower() == 'and': # preconds and effects only use 'and' keyword - exprs = _parse_expr_list(tokens) - expr_lst.extend(exprs) - else: # parse single expression - expr_lst.append(_parse_single_expr_string(deque([token]) + tokens)) - return expr_lst + expr_lst.append(_parse_single_expr_string(token)) + return expr_lst + + +def _parse_formula(tokens: deque) -> list: + if not isinstance(tokens, Deque) and not tokens: + raise ParseError('Expected formula') + expr_lst = [] + token = tokens.popleft() + if token.lower() == 'and': # preconds and effects only use 'and' keyword + exprs = _parse_expr_list(tokens) + expr_lst.extend(exprs) + else: # parse single expression + expr_lst.append(_parse_single_expr_string(deque([token]) + tokens)) + return expr_lst class DomainParser: def __init__(self): + self.filename = '' self.domain_name = '' self._action_name = '' self._requirements = [] @@ -351,32 +167,75 @@ def __init__(self): self._preconditions = [] self._effects = [] + def _parse_define(self, tokens: deque) -> bool: + if not isinstance(tokens, Deque): + raise ParseError('Domain list not found after define statement') + domain_seq = tokens.popleft() + if not domain_seq: + raise ParseError('Domain list empty') + token = domain_seq.popleft() + if token != 'domain': + raise ParseError('Domain keyword not found after define statement') + if not domain_seq: + 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 isinstance(tokens, Deque): + 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. ' + + 'Cannot parse this domain file {}'.format(self.filename)) + return True + def _parse_constants(self, tokens: deque) -> bool: - self.constants = parse_variables(tokens, self._types) + if not isinstance(tokens, Deque): + 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 isinstance(tokens, Deque): + raise ParseError('Expected list of types') self._types = True return True def _parse_predicates(self, tokens: deque) -> bool: while tokens: + if not isinstance(tokens, Deque): + raise ParseError('Valid list not found after :predicates keyword') predicate = tokens.popleft() + if not isinstance(predicate, Deque): + raise ParseError('Invalid predicate: {}'.format(predicate)) pred_name = predicate.popleft() - new_predicate = [pred_name] + parse_variables(predicate, self._types) + if not isinstance(pred_name, str): + raise ParseError('Invalid predicate name: {}'.format(pred_name)) + if not isinstance(predicate, Deque): + 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 isinstance(tokens, Deque): + raise ParseError('Invalid action: {}'.format(tokens)) self._action_name = tokens.popleft() + if not isinstance(self._action_name, str): + 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[0] for p in self._parameters] - action = (build_expr_string(self._action_name, params), self._preconditions, self._effects) + 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 = '' @@ -386,59 +245,94 @@ def _parse_action(self, tokens) -> bool: return True def _parse_parameters(self, tokens: deque) -> bool: - if tokens: + if isinstance(tokens, Deque) and tokens: param_list = tokens.popleft() - self._parameters = parse_variables(param_list, self._types) + if not isinstance(param_list, Deque): + 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 tokens: - precond_seq = tokens.popleft() - self._preconditions = parse_formula(precond_seq) + if not isinstance(tokens, Deque) or not tokens: + raise ParseError('Invalid precondition list for action {}: {}'.format(self._action_name, tokens)) + precond_seq = tokens.popleft() + self._preconditions = _parse_formula(precond_seq) return True def _parse_effects(self, tokens: deque) -> bool: - if tokens: - effects_seq = tokens.popleft() - self._effects = parse_formula(effects_seq) + if not isinstance(tokens, Deque) or not tokens: + raise ParseError('Invalid effects list for action {}: {}'.format(self._action_name, tokens)) + effects_seq = tokens.popleft() + self._effects = _parse_formula(effects_seq) return True def read(self, filename) -> None: pddl = read_pddl_file(filename) - parsers = [DefineDomain(), Requirements()] - parse_tokens(parsers, pddl) + self.filename = os.path.basename(filename) + + # 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) class ProblemParser: def __init__(self): + self.filename = '' self.problem_name = '' self.domain_name = '' self.initial_state = [] self.goals = [] def _parse_define(self, tokens: deque) -> bool: - problem_list = tokens.popleft() - token = problem_list.popleft() + if not isinstance(tokens, Deque) or not tokens: + raise ParseError('Expected problem list after define statement') + problem_seq = tokens.popleft() + if not isinstance(problem_seq, Deque) or not problem_seq: + raise ParseError('Problem list empty') + token = problem_seq.popleft() if token != 'problem': - raise ParseError('problem keyword not found after define statement') - self.problem_name = problem_list.popleft() + 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 isinstance(tokens, Deque) or not tokens: + raise ParseError('Expected domain name') 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 isinstance(tokens, Deque) or not tokens: + raise ParseError('Valid list not found after :goal keyword') goal_list = tokens.popleft() - self.goals = parse_formula(goal_list) + self.goals = _parse_formula(goal_list) return True def read(self, filename) -> None: pddl = read_pddl_file(filename) - parsers = [DefineProblem()] - parse_tokens(parsers, pddl) + self.filename = os.path.basename(filename) + + # 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) diff --git a/planning.py b/planning.py index 707feda2b..876ab8b6d 100644 --- a/planning.py +++ b/planning.py @@ -1,6 +1,5 @@ """Planning (Chapters 10-11) """ -import sys import os from logic import fol_bc_and from utils import expr, Expr, partition @@ -210,24 +209,19 @@ def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: def gather_test_pairs() -> list: - pddl_direntries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('.pddl')] + domain_entries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('domain.pddl')] + problem_entries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('problem.pddl')] domain_objects = [] problem_objects = [] - for de in pddl_direntries: - try: - domain_parser = DomainParser() - domain_parser.read(de.path) - domain_objects.append(domain_parser) - except ParseError as pe1: - try: - problem_parser = ProblemParser() - problem_parser.read(de.path) - problem_objects.append(problem_parser) - except ParseError as pe2: - exc_text = "Unable to recognize format of {}\n".format(de.name) - exc_text += pe1.args[0] + '\n' - exc_text += pe2.args[0] + '\n' - raise ParseError(exc_text) + for de in domain_entries: + domain_parser = DomainParser() + domain_parser.read(de.path) + domain_objects.append(domain_parser) + + for de in problem_entries: + problem_parser = ProblemParser() + problem_parser.read(de.path) + problem_objects.append(problem_parser) object_pairs = [] for p in problem_objects: @@ -240,135 +234,10 @@ def gather_test_pairs() -> list: raise ParseError('No matching PDDL domain and problem files found.') -def air_cargo(): - goals = [expr('At(C1, JFK)'), expr('At(C2, SFO)')] - - init = PlanningKB(goals, - [expr('At(C1, SFO)'), - expr('At(C2, JFK)'), - expr('At(P1, SFO)'), - expr('At(P2, JFK)'), - expr('Cargo(C1)'), - expr('Cargo(C2)'), - expr('Plane(P1)'), - expr('Plane(P2)'), - expr('Airport(JFK)'), - expr('Airport(SFO)')]) - - # Actions - # Load - precond = [expr('At(c, a)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] - effect = [expr('In(c, p)'), expr('~At(c, a)')] - load = PlanningAction(expr('Load(c, p, a)'), precond, effect) - - # Unload - precond = [expr('In(c, p)'), expr('At(p, a)'), expr('Cargo(c)'), expr('Plane(p)'), expr('Airport(a)')] - effect = [expr('At(c, a)'), expr('~In(c, p)')] - unload = PlanningAction(expr('Unload(c, p, a)'), precond, effect) - - # Fly - # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function - precond = [expr('At(p, f)'), expr('Plane(p)'), expr('Airport(f)'), expr('Airport(to)')] - effect = [expr('At(p, to)'), expr('~At(p, f)')] - fly = PlanningAction(expr('Fly(p, f, to)'), precond, effect) - - p = PlanningProblem(init, [load, unload, fly]) - print_solution(astar_search(p)) - - -def spare_tire(): - goals = [expr('At(Spare, Axle)')] - init = PlanningKB(goals, - [expr('At(Flat, Axle)'), - expr('At(Spare, Trunk)')]) - # Actions - # Remove(Spare, Trunk) - precond = [expr('At(Spare, Trunk)')] - effect = [expr('At(Spare, Ground)'), expr('~At(Spare, Trunk)')] - remove_spare = PlanningAction(expr('Remove(Spare, Trunk)'), precond, effect) - # Remove(Flat, Axle) - precond = [expr('At(Flat, Axle)')] - effect = [expr('At(Flat, Ground)'), expr('~At(Flat, Axle)')] - remove_flat = PlanningAction(expr('Remove(Flat, Axle)'), precond, effect) - # PutOn(Spare, Axle) - precond = [expr('At(Spare, Ground)'), expr('~At(Flat, Axle)')] - effect = [expr('At(Spare, Axle)'), expr('~At(Spare, Ground)')] - put_on_spare = PlanningAction(expr('PutOn(Spare, Axle)'), precond, effect) - # LeaveOvernight - precond = [] - effect = [expr('~At(Spare, Ground)'), expr('~At(Spare, Axle)'), expr('~At(Spare, Trunk)'), - expr('~At(Flat, Ground)'), expr('~At(Flat, Axle)')] - leave_overnight = PlanningAction(expr('LeaveOvernight'), precond, effect) - p = PlanningProblem(init, [remove_spare, remove_flat, put_on_spare, leave_overnight]) - print_solution(astar_search(p)) - - -def sussman_anomaly(): - goals = [expr('On(A, B)'), expr('On(B, C)')] - init = PlanningKB(goals, - [expr('On(A, Table)'), - expr('On(B, Table)'), - expr('On(C, A)'), - expr('Block(A)'), - expr('Block(B)'), - expr('Block(C)'), - expr('Clear(B)'), - expr('Clear(C)')]) - - # Actions - # Move(b, x, y) - precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Clear(y)'), expr('Block(b)')] - effect = [expr('On(b, y)'), expr('Clear(x)'), expr('~On(b, x)'), expr('~Clear(y)')] - move = PlanningAction(expr('Move(b, x, y)'), precond, effect) - - # MoveToTable(b, x) - precond = [expr('On(b, x)'), expr('Clear(b)'), expr('Block(b)')] - effect = [expr('On(b, Table)'), expr('Clear(x)'), expr('~On(b, x)')] - move_to_table = PlanningAction(expr('MoveToTable(b, x)'), precond, effect) - - p = PlanningProblem(init, [move, move_to_table]) - print_solution(astar_search(p)) - - -def put_on_shoes(): - goals = [expr('On(RightShoe, RF)'), expr('On(LeftShoe, LF)')] - init = PlanningKB(goals, [expr('Clear(LF)'), - expr('Clear(RF)'), - expr('LeftFoot(LF)'), - expr('RightFoot(RF)')]) - - # Actions - # RightShoe - precond = [expr('On(RightSock, x)'), expr('RightFoot(x)'), expr('~On(RightShoe, x)')] - effect = [expr('On(RightShoe, x)')] - right_shoe = PlanningAction(expr('RightShoeOn'), precond, effect) - - # RightSock - precond = [expr('Clear(x)'), expr('RightFoot(x)')] - effect = [expr('On(RightSock, x)'), expr('~Clear(x)')] - right_sock = PlanningAction(expr('RightSockOn'), precond, effect) - - # LeftShoe - precond = [expr('On(LeftSock, x)'), expr('LeftFoot(x)'), expr('~On(LeftShoe, x)')] - effect = [expr('On(LeftShoe, x)')] - left_shoe = PlanningAction(expr('LeftShoeOn'), precond, effect) - - # LeftSock - precond = [expr('Clear(x)'), expr('LeftFoot(x)')] - effect = [expr('On(LeftSock, x)'), expr('~Clear(x)')] - left_sock = PlanningAction(expr('LeftSockOn'), precond, effect) - - p = PlanningProblem(init, [right_shoe, right_sock, left_shoe, left_sock]) - print_solution(astar_search(p)) - - def test_solutions(): for domain, problem in gather_test_pairs(): construct_solution_from_pddl(domain, problem) if __name__ == '__main__': - air_cargo() - spare_tire() - sussman_anomaly() - put_on_shoes() + test_solutions() From 5ee73c1b6acf933819957b7fad9e19998eacd65f Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 14 May 2018 21:51:21 -0400 Subject: [PATCH 25/40] Change to exception in gather_test_pairs() --- planning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/planning.py b/planning.py index 876ab8b6d..e3478b369 100644 --- a/planning.py +++ b/planning.py @@ -4,7 +4,7 @@ from logic import fol_bc_and from utils import expr, Expr, partition from search import astar_search -from pddl_parse import DomainParser, ProblemParser, ParseError +from pddl_parse import DomainParser, ProblemParser class PlanningKB: @@ -231,7 +231,7 @@ def gather_test_pairs() -> list: if object_pairs: return object_pairs else: - raise ParseError('No matching PDDL domain and problem files found.') + raise IOError('No matching PDDL domain and problem files found.') def test_solutions(): From f27fe347bc0ae0999effd727fa3ea482e48fd87e Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Tue, 15 May 2018 23:08:34 -0400 Subject: [PATCH 26/40] Update config files --- .idea/dictionaries/brandon_corfman.xml | 3 +++ .idea/misc.xml | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .idea/dictionaries/brandon_corfman.xml create mode 100644 .idea/misc.xml 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 From 8f46e6626b52b6237ef6712683bd15e0071245d7 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 27 May 2018 23:25:44 -0400 Subject: [PATCH 27/40] Resolved conflicts. --- logic.py | 2 +- planning.py | 505 +++++++++++++++++++++++++--------------------------- 2 files changed, 240 insertions(+), 267 deletions(-) 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/planning.py b/planning.py index f0ade5fce..5ecf61227 100644 --- a/planning.py +++ b/planning.py @@ -1,58 +1,12 @@ """Planning (Chapters 10-11) """ import os +import itertools +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 search import astar_search from pddl_parse import DomainParser, ProblemParser -import itertools -from search import Node -from collections import deque - - -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 = [] - self.goal_clauses = frozenset(goals) - self.clause_set = frozenset(initial_clauses) - - 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 - - # heuristic 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.""" - 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): - """ Returns: number of remaining goal clauses to be satisfied """ - return len(self.goal_clauses - self.clause_set) - - def fetch_rules_for_goal(self, goal): - return self.clause_set class PDDL: @@ -188,193 +142,6 @@ def act(self, kb, args): return kb -class PlanningProblem: - """ - Used to define a planning problem. - It stores states in a knowledge base consisting 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 - - def __repr__(self): - return '{}({}, {})'.format(self.__class__.__name__, self.initial, self.possible_actions) - - def actions(self, state): - for action in self.possible_actions: - for subst in action.check_precond(state): - new_action = action.copy() - new_action.subst = subst - 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 PlanningAction: - """ - 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): - self.expression = expression - self.name = expression.op - self.args = expression.args - self.subst = None - self.preconds = preconds - 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 - self.effects = effects - 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 - - def __repr__(self): - return '{}({}, {}, {})'.format(self.__class__.__name__, Expr(self.name, *self.args), - list(self.preconds), list(self.effects)) - - 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.preconds = self.preconds.copy() - act.precond_pos = self.precond_pos.copy() - act.precond_neg = self.precond_neg.copy() - act.effects = self.effects.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): - for s in subst: - for _ in fol_bc_and(kb, list(precond), s): - # if any negative preconditions are satisfied by the substitution, then exit loop. - if precond: - break - else: - 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. - if clause_set.isdisjoint(neg_precond): - yield s - - def check_pos_precond(self, kb, precond, subst): - clause_set = kb.fetch_rules_for_goal(None) - 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? - if clause_set.issuperset(pos_precond): - yield s - - def check_precond(self, kb): - """Checks if preconditions are satisfied in the current state""" - yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) - - 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): - for action in node.solution(): - print(action.name, end='(') - for a in action.args[:-1]: - print('{},'.format(action.subst.get(a, a)), end=' ') - if action.args: - print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) - else: - print(')') - print() - - -def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: - initial_kb = PlanningKB([expr(g) for g in pddl_problem.goals], - [expr(s) for s in pddl_problem.initial_state]) - - planning_actions = [PlanningAction(expr(name), - [expr(p) for p in preconds], - [expr(e) for e in effects]) - for name, preconds, effects in pddl_domain.actions] - p = PlanningProblem(initial_kb, planning_actions) - print('\n{} solution:'.format(pddl_problem.problem_name)) - print_solution(astar_search(p)) - - -def gather_test_pairs() -> list: - domain_entries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('domain.pddl')] - problem_entries = [de for de in os.scandir(os.getcwd() + os.sep + 'pddl_files') if de.name.endswith('problem.pddl')] - domain_objects = [] - problem_objects = [] - for de in domain_entries: - domain_parser = DomainParser() - domain_parser.read(de.path) - domain_objects.append(domain_parser) - - for de in problem_entries: - problem_parser = ProblemParser() - problem_parser.read(de.path) - 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 test_solutions(): - for domain, problem in gather_test_pairs(): - construct_solution_from_pddl(domain, problem) - - class Level: """ Contains the state of the planning problem @@ -849,36 +616,6 @@ def linearize(solution): return linear_solution -def double_tennis_problem(): - init = [expr('At(A, LeftBaseLine)'), - expr('At(B, RightNet)'), - expr('Approaching(Ball, RightBaseLine)'), - expr('Partner(A, B)'), - expr('Partner(B, A)')] - - def goal_test(kb): - required = [expr('Returned(Ball)'), expr('At(a, LeftNet)'), expr('At(a, RightNet)')] - return all(kb.ask(q) is not False for q in required) - - # Actions - - # Hit - precond_pos = [expr("Approaching(Ball,loc)"), expr("At(actor,loc)")] - precond_neg = [] - effect_add = [expr("Returned(Ball)")] - effect_rem = [] - hit = Action(expr("Hit(actor, Ball, loc)"), [precond_pos, precond_neg], [effect_add, effect_rem]) - - # Go - precond_pos = [expr("At(actor, loc)")] - precond_neg = [] - effect_add = [expr("At(actor, to)")] - effect_rem = [expr("At(actor, loc)")] - go = Action(expr("Go(actor, to, loc)"), [precond_pos, precond_neg], [effect_add, effect_rem]) - - return PDDL(init, [hit, go], goal_test) - - class HLA(Action): """ Define Actions for the real-world (that may be refined further), and satisfy resource @@ -1172,3 +909,239 @@ def goal_test(kb): return Problem(init, [add_engine1, add_engine2, add_wheels1, add_wheels2, inspect1, inspect2], goal_test, [job_group1, job_group2], 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 = [] + self.goal_clauses = frozenset(goals) + self.clause_set = frozenset(initial_clauses) + + 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 PlanningProblem: + """ + Used to define a planning problem. + It stores states in a knowledge base consisting 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 + + def __repr__(self): + return '{}({}, {})'.format(self.__class__.__name__, self.initial, self.possible_actions) + + def actions(self, state): + for action in self.possible_actions: + for subst in action.check_precond(state): + new_action = action.copy() + new_action.subst = subst + 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 PlanningAction: + """ + 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): + 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 + + 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 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): + for s in subst: + for _ in fol_bc_and(kb, list(precond), s): + # if any negative preconditions are satisfied by the substitution, then exit loop. + if precond: + break + else: + 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. + if clause_set.isdisjoint(neg_precond): + yield s + + def check_pos_precond(self, kb, precond, subst): + clause_set = kb.fetch_rules_for_goal(None) + 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? + if clause_set.issuperset(pos_precond): + yield s + + def check_precond(self, kb): + """Checks if preconditions are satisfied in the current state""" + yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) + + 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(action.subst.get(a, a)), end=' ') + if action.args: + print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) + else: + print(')') + print() + + +def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: + initial_kb = PlanningKB([expr(g) for g in pddl_problem.goals], + [expr(s) for s in pddl_problem.initial_state]) + + planning_actions = [PlanningAction(expr(name), + [expr(p) for p in preconds], + [expr(e) for e in effects]) + for name, preconds, effects in pddl_domain.actions] + p = PlanningProblem(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 test_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) From 920b965ccbdb3d06a1585c69b828ecc2263aeec2 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 28 May 2018 15:08:35 -0400 Subject: [PATCH 28/40] Solved conflicts with current version of planning.py, plus added new PDDL examples. --- pddl_files/cake-domain.pddl | 20 +++++ pddl_files/cake-problem.pddl | 10 +++ pddl_files/shoes-domain.pddl | 8 +- pddl_files/shopping-domain.pddl | 20 +++++ pddl_files/shopping-problem.pddl | 17 +++++ planning.py | 121 +++++++++++++++++++++++++++---- 6 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 pddl_files/cake-domain.pddl create mode 100644 pddl_files/cake-problem.pddl create mode 100644 pddl_files/shopping-domain.pddl create mode 100644 pddl_files/shopping-problem.pddl diff --git a/pddl_files/cake-domain.pddl b/pddl_files/cake-domain.pddl new file mode 100644 index 000000000..57e8941f5 --- /dev/null +++ b/pddl_files/cake-domain.pddl @@ -0,0 +1,20 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Cake domain +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (domain Cake) + (:requirements :strips) + + (:action Eat + :parameters (?x) + :precondition (have ?x) + :effect (and (eaten ?x) (not (have ?x))) + ) + + (:action Bake + :parameters (?x) + :precondition (not (have ?x)) + :effect (have ?x) + ) +) + diff --git a/pddl_files/cake-problem.pddl b/pddl_files/cake-problem.pddl new file mode 100644 index 000000000..932b5a126 --- /dev/null +++ b/pddl_files/cake-problem.pddl @@ -0,0 +1,10 @@ +; The "have cake and eat it too" problem. +; Good case to test failure since it can't be solved. + +(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 index 183759603..532e4e8db 100755 --- a/pddl_files/shoes-domain.pddl +++ b/pddl_files/shoes-domain.pddl @@ -9,25 +9,25 @@ (on ?x ?y) ) - (:action Right_Shoe_On + (:action RightShoe :parameters () :precondition (and (on RightSock ?x) (rightfoot ?x) (not (on RightShoe ?x))) :effect (and (on RightShoe ?x)) ) - (:action Right_Sock_On + (:action RightSock :parameters () :precondition (and (clear ?x) (rightfoot ?x)) :effect (and (on RightSock ?x) (not (clear ?x))) ) - (:action Left_Shoe_On + (:action LeftShoe :parameters () :precondition (and (on LeftSock ?x) (leftfoot ?x) (not (on LeftShoe ?x))) :effect (and (on LeftShoe ?x)) ) - (:action Left_Sock_On + (:action LeftSock :parameters () :precondition (and (clear ?x) (leftfoot ?x)) :effect (and (on LeftSock ?x) (not (clear ?x))) 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/planning.py b/planning.py index 1e89cea94..6a531e21b 100644 --- a/planning.py +++ b/planning.py @@ -391,6 +391,113 @@ def extract_solution(self, goals, index): return solution + def goal_test(self, kb): + return all(kb.ask(q) is not False for q in self.graph.pddl.goals) + + def execute(self): + """Executes the GraphPlan algorithm for the given problem""" + + while True: + self.graph.expand_graph() + if (self.goal_test(self.graph.levels[-1].kb) and self.graph.non_mutex_goals(self.graph.pddl.goals, -1)): + 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 + + +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""" @@ -480,20 +587,6 @@ def goal_test(kb, goals): return None - def execute(self): - """Executes the GraphPlan algorithm for the given problem""" - - while True: - self.graph.expand_graph() - if (self.goal_test(self.graph.levels[-1].kb) and self.graph.non_mutex_goals(self.graph.pddl.goals, -1)): - 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 - - def socks_and_shoes_graphplan(): pddl = socks_and_shoes() graphplan = GraphPlan(pddl) From 41aeaf416f0a9effead3168d7994441fac65ce29 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 28 May 2018 17:11:53 -0400 Subject: [PATCH 29/40] Accidentally deleted TotalOrderPlanner in the last merge, put it back in. --- planning.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/planning.py b/planning.py index 6a531e21b..a9c1a3be9 100644 --- a/planning.py +++ b/planning.py @@ -606,6 +606,55 @@ def goal_test(kb, goals): if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff(): return None +class TotalOrderPlanner: + + def __init__(self, pddl): + self.pddl = pddl + + def filter(self, solution): + """Filter out persistence actions from a solution""" + + new_solution = [] + for section in solution[0]: + new_section = [] + for operation in section: + if not (operation.op[0] == 'P' and operation.op[1].isupper()): + new_section.append(operation) + new_solution.append(new_section) + return new_solution + + def orderlevel(self, level, pddl): + """Return valid linear order of actions for a given level""" + + for permutation in itertools.permutations(level): + temp = copy.deepcopy(pddl) + count = 0 + for action in permutation: + try: + temp.act(action) + count += 1 + except: + count = 0 + temp = copy.deepcopy(pddl) + break + if count == len(permutation): + return list(permutation), temp + return None + + def execute(self): + """Finds total-order solution for a planning graph""" + + graphplan_solution = GraphPlan(self.pddl).execute() + filtered_solution = self.filter(graphplan_solution) + ordered_solution = [] + pddl = self.pddl + for level in filtered_solution: + level_solution, pddl = self.orderlevel(level, pddl) + for element in level_solution: + ordered_solution.append(element) + + return ordered_solution + def linearize(solution): """Converts a level-ordered solution into a linear solution""" From fd4e362570bef59df625cacaf8203f60939b207a Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 28 May 2018 17:15:07 -0400 Subject: [PATCH 30/40] Added import of copy module. --- planning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/planning.py b/planning.py index a9c1a3be9..4b22a2782 100644 --- a/planning.py +++ b/planning.py @@ -1,6 +1,7 @@ """Planning (Chapters 10-11) """ import os +import copy import itertools from search import Node, astar_search from collections import deque From 1ed503162c3ecfa04542675510fdb05038a5b681 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 28 May 2018 21:38:48 -0400 Subject: [PATCH 31/40] Changed _build_expr_string to be accessible outside pddl_parse module. Fixed bug on build_expr_string to correctly capitalize negative Exprs. Created classmethods on PlanningProblem and PlanningAction to __init__ those classes from existing PDDL and Action objects. Fixed __repr__ method on PlanningAction. --- pddl_parse.py | 11 +++++++---- planning.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/pddl_parse.py b/pddl_parse.py index c7902dc1c..c52f3d80e 100644 --- a/pddl_parse.py +++ b/pddl_parse.py @@ -81,9 +81,12 @@ def match_tokens(tokens: deque): break -def _build_expr_string(expr_name: str, variables: list) -> str: +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 - estr = expr_name.replace('-', '').capitalize() + '(' + 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): @@ -145,7 +148,7 @@ def _parse_single_expr_string(tokens: deque) -> str: if not param[0].isupper(): param = param.capitalize() variables.append(param) - return _build_expr_string(expr_name, variables) + return build_expr_string(expr_name, variables) def _parse_expr_list(tokens) -> list: @@ -259,7 +262,7 @@ def _parse_action(self, tokens) -> bool: } parse_tokens(match, tokens) params = [p for p in self._parameters] - action = (_build_expr_string(self._action_name, params), self._preconditions, self._effects) + 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 = '' diff --git a/planning.py b/planning.py index 4b22a2782..f835ac1f3 100644 --- a/planning.py +++ b/planning.py @@ -7,7 +7,7 @@ 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 +from pddl_parse import DomainParser, ProblemParser, build_expr_string class PDDL: @@ -607,8 +607,8 @@ def goal_test(kb, goals): if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff(): return None -class TotalOrderPlanner: +class TotalOrderPlanner: def __init__(self, pddl): self.pddl = pddl @@ -975,6 +975,14 @@ 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(PlanningAction.from_action(act)) + return cls(initial, planning_actions) + def __repr__(self): return '{}({}, {})'.format(self.__class__.__name__, self.initial, self.possible_actions) @@ -1035,9 +1043,25 @@ def __init__(self, expression, preconds, effects): 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)))) + 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) From dc61a2d568095768ff163c060c9b49915bf1d9ab Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 4 Jun 2018 06:48:48 -0400 Subject: [PATCH 32/40] Yield blank set from check_pos_precond for failure case. --- planning.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/planning.py b/planning.py index f835ac1f3..bc46631a6 100644 --- a/planning.py +++ b/planning.py @@ -1097,11 +1097,14 @@ def check_neg_precond(self, kb, precond, subst): def check_pos_precond(self, kb, precond, subst): clause_set = kb.fetch_rules_for_goal(None) - 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? - if clause_set.issuperset(pos_precond): - yield s + if not precond: + yield {} + else: + 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? + if clause_set.issuperset(pos_precond): + yield s def check_precond(self, kb): """Checks if preconditions are satisfied in the current state""" @@ -1177,3 +1180,7 @@ def test_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) + + +if __name__ == '__main__': + test_planning_solutions() \ No newline at end of file From 3ce0bd35972eee767d844d1a6da9d9ee028e761f Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 11 Jun 2018 00:43:45 -0400 Subject: [PATCH 33/40] Solved bug exposed with Cake problem by passing a flag back indicating whether action is valid (separate from one or more valid substitutions). --- pddl_files/cake-domain.pddl | 12 +++++----- pddl_files/cake-problem.pddl | 1 - planning.py | 45 +++++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/pddl_files/cake-domain.pddl b/pddl_files/cake-domain.pddl index 57e8941f5..5bae439ed 100644 --- a/pddl_files/cake-domain.pddl +++ b/pddl_files/cake-domain.pddl @@ -6,15 +6,15 @@ (:requirements :strips) (:action Eat - :parameters (?x) - :precondition (have ?x) - :effect (and (eaten ?x) (not (have ?x))) + :parameters (Cake) + :precondition (have Cake) + :effect (and (eaten Cake) (not (have Cake))) ) (:action Bake - :parameters (?x) - :precondition (not (have ?x)) - :effect (have ?x) + :parameters (Cake) + :precondition (not (have Cake)) + :effect (have Cake) ) ) diff --git a/pddl_files/cake-problem.pddl b/pddl_files/cake-problem.pddl index 932b5a126..b0229604a 100644 --- a/pddl_files/cake-problem.pddl +++ b/pddl_files/cake-problem.pddl @@ -1,5 +1,4 @@ ; The "have cake and eat it too" problem. -; Good case to test failure since it can't be solved. (define (problem HaveCakeAndEatItToo) (:domain Cake) diff --git a/planning.py b/planning.py index bc46631a6..448f9f076 100644 --- a/planning.py +++ b/planning.py @@ -988,10 +988,11 @@ def __repr__(self): def actions(self, state): for action in self.possible_actions: - for subst in action.check_precond(state): - new_action = action.copy() - new_action.subst = subst - yield new_action + for valid, subst in action.check_precond(state): + if valid: + new_action = action.copy() + new_action.subst = subst + yield new_action def goal_test(self, state): return state.goal_test() @@ -1083,32 +1084,38 @@ def substitute(self, subst, e): return Expr(e.op, *new_args) def check_neg_precond(self, kb, precond, subst): - for s in subst: - for _ in fol_bc_and(kb, list(precond), s): - # if any negative preconditions are satisfied by the substitution, then exit loop. - if precond: - break - else: + 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. - if clause_set.isdisjoint(neg_precond): - yield s + 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): - clause_set = kb.fetch_rules_for_goal(None) - if not precond: - yield {} - else: + 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? - if clause_set.issuperset(pos_precond): - yield s + clause_set = kb.fetch_rules_for_goal(None) + found_subst = True + yield clause_set.issuperset(pos_precond), s + if not found_subst: + yield True, subst + else: + yield True, subst def check_precond(self, kb): """Checks if preconditions are satisfied in the current state""" - yield from self.check_neg_precond(kb, self.precond_neg, self.check_pos_precond(kb, self.precond_pos, {})) + 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 """ From c26f0a2731393a938fa8d004d95cfb48c180aaf5 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Mon, 11 Jun 2018 00:46:29 -0400 Subject: [PATCH 34/40] Removed test runner from bottom of module. --- planning.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/planning.py b/planning.py index 448f9f076..ac4222c6e 100644 --- a/planning.py +++ b/planning.py @@ -1187,7 +1187,3 @@ def test_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) - - -if __name__ == '__main__': - test_planning_solutions() \ No newline at end of file From 556b0723d1fed70d4353358406a4676ec1abcbab Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Thu, 14 Jun 2018 21:59:45 -0400 Subject: [PATCH 35/40] At Dr. Norvig's request, updated PlanningAction and PlanningKB to use strings or Exprs for a simplified API. --- planning.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/planning.py b/planning.py index ac4222c6e..dab3c7a70 100644 --- a/planning.py +++ b/planning.py @@ -925,9 +925,13 @@ class PlanningKB: def __init__(self, goals, initial_clauses=None): if initial_clauses is None: initial_clauses = [] - self.goal_clauses = frozenset(goals) + + 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__): @@ -967,8 +971,8 @@ def fetch_rules_for_goal(self, goal): class PlanningProblem: """ - Used to define a planning problem. - It stores states in a knowledge base consisting of first order logic statements. + 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): @@ -1034,6 +1038,12 @@ class PlanningAction: """ 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 @@ -1144,14 +1154,10 @@ def print_solution(node): def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: - initial_kb = PlanningKB([expr(g) for g in pddl_problem.goals], - [expr(s) for s in pddl_problem.initial_state]) - - planning_actions = [PlanningAction(expr(name), - [expr(p) for p in preconds], - [expr(e) for e in effects]) - for name, preconds, effects in pddl_domain.actions] + initial_kb = PlanningKB(pddl_problem.goals, pddl_problem.initial_state) + planning_actions = [PlanningAction(name, preconds, effects) for name, preconds, effects in pddl_domain.actions] p = PlanningProblem(initial_kb, planning_actions) + print('\n{} solution:'.format(pddl_problem.problem_name)) print_solution(astar_search(p)) From 2f8799d181cef89ae26f23b8a600564f39eb7535 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 16 Jun 2018 09:54:45 -0400 Subject: [PATCH 36/40] Brief edits to parse.py --- parse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parse.py b/parse.py index 6201c5493..427be8b1b 100644 --- a/parse.py +++ b/parse.py @@ -1,5 +1,5 @@ from collections.abc import MutableSequence -from planning import PlanningAction +from planning import STRIPSAction from utils import expr @@ -180,7 +180,7 @@ def _parse_action(self, tokens) -> bool: parse_tokens(match, tokens) params = [p[0] for p in self.parameters] action_str = build_expr_string(self.action_name, params) - action = PlanningAction(expr(action_str), self.preconditions, self.effects) + action = STRIPSAction(action_str, self.preconditions, self.effects) self.actions.append(action) return True @@ -337,7 +337,7 @@ def _parse_action(self, tokens) -> bool: parse_tokens(match, tokens) params = [p[0] for p in self.parameters] action_str = build_expr_string(self.action_name, params) - action = PlanningAction(expr(action_str), self.preconditions, self.effects) + action = STRIPSAction(action_str, self.preconditions, self.effects) self.actions.append(action) return True From e6fa99d6dfb666b1aa4edb5d8dd22e10325552cf Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 16 Jun 2018 09:59:47 -0400 Subject: [PATCH 37/40] Renamed PlanningProblem to PlanningSearchProblem and PlanningAction to STRIPSAction. --- parse.py | 414 ---------------------------------------------------- planning.py | 10 +- 2 files changed, 5 insertions(+), 419 deletions(-) delete mode 100644 parse.py diff --git a/parse.py b/parse.py deleted file mode 100644 index 427be8b1b..000000000 --- a/parse.py +++ /dev/null @@ -1,414 +0,0 @@ -from collections.abc import MutableSequence -from planning import STRIPSAction -from utils import expr - - -Symbol = str # A Lisp Symbol is implemented as a Python str -List = list # A Lisp List is implemented as a Python list - - -class ParseError(Exception): - pass - - -def read_pddl_file(filename) -> list: - with open(filename) 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 - s = ''.join(lines) - # transform into Python-compatible S-expressions (using lists of strings) - return parse(s) - - -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(pddl): - """Read PDDL contained in a string.""" - return read_from_tokens(tokenize(pddl)) - - -def tokenize(s): - """Convert a string into a list of tokens.""" - return s.replace('(', ' ( ').replace(')', ' ) ').replace(':', ' :').split() - - -def read_from_tokens(tokens): - """Read an expression from a sequence of tokens.""" - if len(tokens) == 0: - raise SyntaxError('unexpected EOF while reading') - token = tokens.pop(0) - if '(' == token: - L = [] - while tokens[0] != ')': - L.append(read_from_tokens(tokens)) - tokens.pop(0) # pop off ')' - # reverse each list so we can continue to use .pop() on it, and the elements will be in order. - L.reverse() - return L - elif ')' == token: - raise SyntaxError('unexpected )') - else: - return token - - -def parse_tokens(match_dict, token_list): - def match_tokens(tokens): - if not tokens: - return False - item = tokens.pop() - if isinstance(item, MutableSequence): - match_tokens(item) - else: - for text in match_dict: - if item.startswith(text): - if match_dict[text](tokens): - break - return True - - while True: - if not match_tokens(token_list): - break - - -def parse_variables(tokens, types) -> list: - variables = [] - num_tokens = len(tokens) - idx = 0 - while idx < num_tokens: - if not tokens[idx].startswith('?'): - raise ParseError("Unrecognized variable name ({0}) " + - "that doesn't begin with a question mark".format(tokens[idx])) - pred_var = tokens[idx][1:] - if not types: - variables.append(pred_var) - idx += 1 - else: - # lookahead to see if there's a dash indicating an upcoming type name - if tokens[idx + 1] == '-': - pred_type = tokens[idx + 2].lower() - if pred_type not in types: - raise ParseError("Predicate type {0} not in type list.".format(pred_type)) - else: - pred_type = None - arg = [pred_var, pred_type] - variables.append(arg) - # if any immediately prior variables didn't have an assigned type, then assign them this one. - for j in range(len(variables) - 1, 0, -1): - if variables[j][1] is not None: - break - else: - variables[j][1] = pred_type - idx += 3 - return variables - - -def build_expr_string(expr_name, variables): - estr = expr_name + '(' - vlen = len(variables) - if vlen: - for i in range(vlen - 1): - estr += variables[i] + ', ' - estr += variables[vlen - 1] - estr += ')' - return estr - - -class PDDLDomainParser: - def __init__(self): - self.domain_name = '' - self.action_name = '' - self.requirements = [] - self.predicates = [] - self.actions = [] - self.types = [] - self.constants = [] - self.parameters = [] - self.preconditions = [] - self.effects = [] - - def _parse_define(self, tokens) -> bool: - domain_list = tokens.pop() - token = domain_list.pop() - if token != 'domain': - raise ParseError('domain keyword not found after define statement') - self.domain_name = domain_list.pop() - return True - - def _parse_requirements(self, tokens) -> bool: - self.requirements = tokens - if ':strips' not in self.requirements: - raise ParseError(':strips is not in list of domain requirements. Cannot parse this domain file.') - return True - - def _parse_constants(self, tokens) -> bool: - self.constants = parse_variables(tokens, self.types) - for const, ctype in self.constants: - if ctype not in self.types: - raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) - return True - - def _parse_types(self, tokens) -> bool: - self.types = tokens - return True - - def _parse_predicates(self, tokens) -> bool: - while tokens: - predicate = tokens.pop() - pred_name = predicate.pop() - predicate.reverse() - new_predicate = [pred_name] + parse_variables(predicate, self.types) - self.predicates.append(new_predicate) - return True - - def _parse_action(self, tokens) -> bool: - self.action_name = tokens.pop() - self.parameters = [] - self.preconditions = [] - self.effects = [] - match = {':parameters': self._parse_parameters, - ':precondition': self._parse_precondition, - ':effect': self._parse_effect - } - parse_tokens(match, tokens) - params = [p[0] for p in self.parameters] - action_str = build_expr_string(self.action_name, params) - action = STRIPSAction(action_str, self.preconditions, self.effects) - self.actions.append(action) - return True - - def _parse_parameters(self, tokens) -> bool: - param_list = tokens.pop() - param_list.reverse() - self.parameters = parse_variables(param_list, self.types) - return True - - def _parse_single_expr_string(self, tokens) -> str: - if tokens[0] == 'not': - token = tokens.pop() - token.reverse() - e = self._parse_single_expr_string(token) - if '~' in e: - raise ParseError('Multiple not operators in expression.') - return '~' + e - else: - expr_name = tokens[0] - variables = [] - idx = 1 - num_tokens = len(tokens) - while idx < num_tokens: - param = tokens[idx] - if param.startswith('?'): - variables.append(param[1:].lower()) - else: - variables.append(param) - idx += 1 - return build_expr_string(expr_name, variables) - - def _parse_expr_list(self, tokens) -> list: - expr_lst = [] - while tokens: - token = tokens.pop() - token.reverse() - e = self._parse_single_expr_string(token) - expr_lst.append(expr(e)) - return expr_lst - - def _parse_formula(self, tokens) -> list: - expr_lst = [] - token = tokens.pop() - if token == 'and': # preconds and effects only use 'and' keyword - exprs = self._parse_expr_list(tokens) - expr_lst.extend(exprs) - else: # parse single expression - e = self._parse_single_expr_string([token] + tokens) - expr_lst.append(expr(e)) - return expr_lst - - def _parse_precondition(self, tokens) -> bool: - precond_list = tokens.pop() - self.preconditions = self._parse_formula(precond_list) - return True - - def _parse_effect(self, tokens) -> bool: - effects_list = tokens.pop() - self.effects = self._parse_formula(effects_list) - return True - - def read(self, filename) -> None: - pddl_list = read_pddl_file(filename) - - # 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_list) - - -class PDDLProblemParser: - def __init__(self, types): - self.problem_name = '' - self.types = types - self.objects = [] - self.init = [] - self.goal = [] - - def _parse_define(self, tokens) -> bool: - problem_list = tokens.pop() - token = problem_list.pop() - if token != 'problem': - raise ParseError('problem keyword not found after define statement') - self.problem_name = problem_list.pop() - return True - - def _parse_objects(self, tokens) -> bool: - self.objects = parse_variables(tokens) - for const, ctype in self.constants: - if ctype not in self.types: - raise ParseError('Constant type {0} not found in list of valid types'.format(ctype)) - return True - - def _parse_types(self, tokens) -> bool: - self.types = tokens - return True - - def _parse_predicates(self, tokens) -> bool: - while tokens: - predicate = tokens.pop() - pred_name = predicate.pop() - predicate.reverse() - new_predicate = [pred_name] + self._parse_variables(predicate) - self.predicates.append(new_predicate) - return True - - def _parse_variables(self, tokens) -> list: - variables = [] - num_tokens = len(tokens) - idx = 0 - while idx < num_tokens: - if not tokens[idx].startswith('?'): - raise ParseError("Unrecognized variable name ({0}) " + - "that doesn't begin with a question mark".format(tokens[idx])) - pred_var = tokens[idx][1:] - if not self.types: - variables.append(pred_var) - idx += 1 - else: - # lookahead to see if there's a dash indicating an upcoming type name - if tokens[idx + 1] == '-': - pred_type = tokens[idx + 2].lower() - if pred_type not in self.types: - raise ParseError("Predicate type {0} not in type list.".format(pred_type)) - else: - pred_type = None - arg = [pred_var, pred_type] - variables.append(arg) - # if any immediately prior variables didn't have an assigned type, then assign them this one. - for j in range(len(variables) - 1, 0, -1): - if variables[j][1] is not None: - break - else: - variables[j][1] = pred_type - idx += 3 - return variables - - def _parse_action(self, tokens) -> bool: - self.action_name = tokens.pop() - self.parameters = [] - self.preconditions = [] - self.effects = [] - match = {':parameters': self._parse_parameters, - ':precondition': self._parse_precondition, - ':effect': self._parse_effect - } - parse_tokens(match, tokens) - params = [p[0] for p in self.parameters] - action_str = build_expr_string(self.action_name, params) - action = STRIPSAction(action_str, self.preconditions, self.effects) - self.actions.append(action) - return True - - def _parse_parameters(self, tokens) -> bool: - param_list = tokens.pop() - param_list.reverse() - self.parameters = self._parse_variables(param_list) - return True - - def _parse_single_expr_string(self, tokens): - if tokens[0] == 'not': - token = tokens.pop() - token.reverse() - e = self._parse_single_expr_string(token) - if '~' in e: - raise ParseError('Multiple not operators in expression.') - return '~' + e - else: - expr_name = tokens[0] - variables = [] - idx = 1 - num_tokens = len(tokens) - while idx < num_tokens: - param = tokens[idx] - if param.startswith('?'): - variables.append(param[1:].lower()) - else: - variables.append(param) - idx += 1 - return build_expr_string(expr_name, variables) - - def _parse_expr_list(self, tokens): - expr_lst = [] - while tokens: - token = tokens.pop() - token.reverse() - e = self._parse_single_expr_string(token) - expr_lst.append(expr(e)) - return expr_lst - - def _parse_formula(self, tokens): - expr_lst = [] - token = tokens.pop() - if token == 'and': # preconds and effects only use 'and' keyword - exprs = self._parse_expr_list(tokens) - expr_lst.extend(exprs) - else: # parse single expression - e = self._parse_single_expr_string([token] + tokens) - expr_lst.append(expr(e)) - return expr_lst - - def _parse_precondition(self, tokens): - precond_list = tokens.pop() - self.preconditions = self._parse_formula(precond_list) - return True - - def _parse_effect(self, tokens): - effects_list = tokens.pop() - self.effects = self._parse_formula(effects_list) - return True - - def read(self, filename): - pddl_list = read_pddl_file(filename) - - # 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, - ':objects': self._parse_objects, - ':init': self._parse_init, - ':goal': self._parse_goal - } - - parse_tokens(match, pddl_list) diff --git a/planning.py b/planning.py index dab3c7a70..885c6975e 100644 --- a/planning.py +++ b/planning.py @@ -969,7 +969,7 @@ def fetch_rules_for_goal(self, goal): return self.clause_set -class PlanningProblem: +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. @@ -984,7 +984,7 @@ 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(PlanningAction.from_action(act)) + planning_actions.append(STRIPSAction.from_action(act)) return cls(initial, planning_actions) def __repr__(self): @@ -1025,7 +1025,7 @@ def is_negative_clause(e): return e.op == '~' and len(e.args) == 1 -class PlanningAction: +class STRIPSAction: """ Defines an action schema using preconditions and effects Use this to describe actions in PDDL @@ -1155,8 +1155,8 @@ def print_solution(node): def construct_solution_from_pddl(pddl_domain, pddl_problem) -> None: initial_kb = PlanningKB(pddl_problem.goals, pddl_problem.initial_state) - planning_actions = [PlanningAction(name, preconds, effects) for name, preconds, effects in pddl_domain.actions] - p = PlanningProblem(initial_kb, planning_actions) + 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)) From a38f29bc2b3aebbe7915cefa032942fc87de0a94 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sat, 16 Jun 2018 10:26:56 -0400 Subject: [PATCH 38/40] Fixed True/False bug in STRIPSAction.check_pos_precond --- planning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planning.py b/planning.py index 885c6975e..7fd812527 100644 --- a/planning.py +++ b/planning.py @@ -1117,7 +1117,7 @@ def check_pos_precond(self, kb, precond, subst): found_subst = True yield clause_set.issuperset(pos_precond), s if not found_subst: - yield True, subst + yield False, subst else: yield True, subst From db83e6f501015bd64c51bd024c950ce6fc7ee771 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 17 Jun 2018 20:14:55 -0400 Subject: [PATCH 39/40] Added three unit tests for PlanningSearchProblem. Added substitution in PlanningSearchProblem.actions() for valid actions. Added __eq__ special method for comparing STRIPSAction objects, both for correctness and simpler unit testing. Simplified print_solution() function since argument substitution went away above. Changed test_planning_solutions() function to run_planning_solutions() so it wouldn't get triggered by py.test. --- planning.py | 18 ++++++++++++++--- tests/test_planning.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/planning.py b/planning.py index 7fd812527..b632906b0 100644 --- a/planning.py +++ b/planning.py @@ -996,6 +996,7 @@ def actions(self, 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): @@ -1076,6 +1077,17 @@ def __repr__(self): 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__) @@ -1145,9 +1157,9 @@ def print_solution(node): for action in node.solution(): print(action.name, end='(') for a in action.args[:-1]: - print('{},'.format(action.subst.get(a, a)), end=' ') + print('{},'.format(a), end=' ') if action.args: - print('{})'.format(action.subst.get(action.args[-1], action.args[-1]))) + print('{})'.format(action.args[-1])) else: print(')') print() @@ -1189,7 +1201,7 @@ def gather_test_pairs() -> list: raise IOError('No matching PDDL domain and problem files found.') -def test_planning_solutions(): +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/tests/test_planning.py b/tests/test_planning.py index 641a2eeca..0c2649619 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) + From 24511f50f05832025994a5b633b2863973124dc8 Mon Sep 17 00:00:00 2001 From: brandon_corfman Date: Sun, 17 Jun 2018 20:21:39 -0400 Subject: [PATCH 40/40] Fixed directory path for pddl_files in PlanningSearchProblem unit tests since it was causing failures. --- tests/test_planning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_planning.py b/tests/test_planning.py index 0c2649619..b8eb14a95 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -262,7 +262,7 @@ def pddl_test_case(domain_file, problem_file, expected_solution): 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') + 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)')] @@ -271,7 +271,7 @@ def test_pddl_have_cake_and_eat_it_too(): def test_pddl_change_flat_tire(): """ Positive precondition test for total-order planner. """ - pddl_dir = os.path.join(os.getcwd(), '..', 'pddl_files') + 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)')] @@ -280,7 +280,7 @@ def test_pddl_change_flat_tire(): def test_pddl_sussman_anomaly(): """ Verifying correct action substitution for total-order planner. """ - pddl_dir = os.path.join(os.getcwd(), '..', 'pddl_files') + 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)')]