From 227848a04e2663af49845b280bbde4698c49af08 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Wed, 23 May 2018 21:48:00 +0530 Subject: [PATCH 01/19] Added PartialOrderPlanner --- planning.py | 373 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 370 insertions(+), 3 deletions(-) diff --git a/planning.py b/planning.py index b5e35dae4..04e9aefc3 100644 --- a/planning.py +++ b/planning.py @@ -5,8 +5,9 @@ import itertools from search import Node from utils import Expr, expr, first -from logic import FolKB, conjuncts +from logic import FolKB, conjuncts, unify from collections import deque +from functools import reduce as _reduce class PDDL: @@ -84,6 +85,9 @@ def __init__(self, action, precond, effect): def __call__(self, kb, args): return self.act(kb, args) + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, Expr(self.name, *self.args)) + def convert(self, clauses): """Converts strings into Exprs""" if isinstance(clauses, Expr): @@ -152,7 +156,7 @@ 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)', + 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)'), @@ -194,6 +198,19 @@ def three_block_tower(): effect='On(b, Table) & Clear(x) & ~On(b, x)')]) +def simple_blocks_world(): + """Simplified blocks world""" + + return PDDL(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)', + goals='On(B, A) & On(C, B)', + actions=[Action('ToTable(x, y)', + precond='On(x, y) & Clear(x)', + effect='~On(x, y) & Clear(y) & OnTable(x)'), + Action('FromTable(y, x)', + precond='OnTable(y) & Clear(y) & Clear(x)', + effect='~OnTable(y) & ~Clear(x) & On(y, x)')]) + + def have_cake_and_eat_cake_too(): """Cake problem""" @@ -239,8 +256,9 @@ def socks_and_shoes(): effect='LeftSockOn')]) -# Doubles tennis problem def double_tennis_problem(): + """Doubles tennis problem""" + return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)', goals='Returned(Ball) & At(a, LeftNet) & At(a, RightNet)', actions=[Action('Hit(actor, Ball, loc)', @@ -573,6 +591,351 @@ def linearize(solution): return linear_solution +''' +[10.13 PartialOrderPlanner: Partially ordered plans are created by a search through the space of plans +rather than a search through the state space. It views planning as a refinement of partially ordered plans. +A partially ordered plan is defined by a set of actions and a set of constraints of the form A < B, +which denotes that action A has to be performed before action B.] +''' + +class PartialOrderPlanner: + + def __init__(self, pddl): + self.pddl = pddl + self.initialize() + + def initialize(self): + """Initialize all variables""" + self.causal_links = [] + self.start = Action('Start', [], self.pddl.init) + self.finish = Action('Finish', self.pddl.goals, []) + self.actions = set() + self.actions.add(self.start) + self.actions.add(self.finish) + self.constraints = set() + self.constraints.add((self.start, self.finish)) + self.agenda = set() + for precond in self.finish.precond: + self.agenda.add((precond, self.finish)) + self.expanded_actions = self.expand_actions() + + def expand_actions(self, name=None): + """Generate all possible actions with variable bindings for precondition selection heuristic""" + + objects = set(arg for clause in self.pddl.init for arg in clause.args) + expansions = [] + action_list = [] + if name is not None: + for action in self.pddl.actions: + if str(action.name) == name: + action_list.append(action) + else: + action_list = self.pddl.actions + + for action in action_list: + for permutation in itertools.permutations(objects, len(action.args)): + bindings = unify(Expr(action.name, *action.args), Expr(action.name, *permutation)) + if bindings is not None: + new_args = [] + for arg in action.args: + if arg in bindings: + new_args.append(bindings[arg]) + else: + new_args.append(arg) + new_expr = Expr(str(action.name), *new_args) + new_preconds = [] + for precond in action.precond: + new_precond_args = [] + for arg in precond.args: + if arg in bindings: + new_precond_args.append(bindings[arg]) + else: + new_precond_args.append(arg) + new_precond = Expr(str(precond.op), *new_precond_args) + new_preconds.append(new_precond) + new_effects = [] + for effect in action.effect: + new_effect_args = [] + for arg in effect.args: + if arg in bindings: + new_effect_args.append(bindings[arg]) + else: + new_effect_args.append(arg) + new_effect = Expr(str(effect.op), *new_effect_args) + new_effects.append(new_effect) + expansions.append(Action(new_expr, new_preconds, new_effects)) + + return expansions + + def find_open_precondition(self): + """Find open precondition with the least number of possible actions""" + + number_of_ways = dict() + actions_for_precondition = dict() + for element in self.agenda: + open_precondition = element[0] + possible_actions = list(self.actions) + self.expanded_actions + for action in possible_actions: + for effect in action.effect: + if effect == open_precondition: + if open_precondition in number_of_ways: + number_of_ways[open_precondition] += 1 + actions_for_precondition[open_precondition].append(action) + else: + number_of_ways[open_precondition] = 1 + actions_for_precondition[open_precondition] = [action] + + number = sorted(number_of_ways, key=number_of_ways.__getitem__) + + for k, v in number_of_ways.items(): + if v == 0: + return None, None, None + + act1 = None + for element in self.agenda: + if element[0] == number[0]: + act1 = element[1] + break + + if number[0] in self.expanded_actions: + self.expanded_actions.remove(number[0]) + + return number[0], act1, actions_for_precondition[number[0]] + + def find_action_for_precondition(self, oprec): + """Find action for a given precondition""" + + # either + # choose act0 E Actions such that act0 achieves G + for action in self.actions: + for effect in action.effect: + if effect == oprec: + return action, 0 + + # or + # choose act0 E Actions such that act0 achieves G + for action in self.pddl.actions: + for effect in action.effect: + if effect.op == oprec.op: + bindings = unify(effect, oprec) + if bindings is None: + break + return action, bindings + + def generate_expr(self, clause, bindings): + """Generate atomic expression from generic expression given variable bindings""" + + new_args = [] + for arg in clause.args: + if arg in bindings: + new_args.append(bindings[arg]) + else: + new_args.append(arg) + + try: + return Expr(str(clause.name), *new_args) + except: + return Expr(str(clause.op), *new_args) + + def generate_action_object(self, action, bindings): + """Generate action object given a generic action andvariable bindings""" + + # if bindings is 0, it means the action already exists in self.actions + if bindings == 0: + return action + + # bindings cannot be None + else: + new_expr = self.generate_expr(action, bindings) + new_preconds = [] + for precond in action.precond: + new_precond = self.generate_expr(precond, bindings) + new_preconds.append(new_precond) + new_effects = [] + for effect in action.effect: + new_effect = self.generate_expr(effect, bindings) + new_effects.append(new_effect) + return Action(new_expr, new_preconds, new_effects) + + def cyclic(self, graph): + """Check cyclicity of a directed graph""" + + new_graph = dict() + for element in graph: + if element[0] in new_graph: + new_graph[element[0]].append(element[1]) + else: + new_graph[element[0]] = [element[1]] + + path = set() + + def visit(vertex): + path.add(vertex) + for neighbor in new_graph.get(vertex, ()): + if neighbor in path or visit(neighbor): + return True + path.remove(vertex) + return False + + value = any(visit(v) for v in new_graph) + return value + + def add_const(self, constraint, constraints): + """Add the constraint to constraints if the resulting graph is acyclic""" + + if constraint[0] == self.finish or constraint[1] == self.start: + return constraints + + new_constraints = set(constraints) + new_constraints.add(constraint) + + if self.cyclic(new_constraints): + return constraints + return new_constraints + + def is_a_threat(self, precondition, effect): + """Check if effect is a threat to precondition""" + + if (str(effect.op) == 'Not' + str(precondition.op)) or ('Not' + str(effect.op) == str(precondition.op)): + if effect.args == precondition.args: + return True + return False + + def protect(self, causal_link, action, constraints): + """Check and resolve threats by promotion or demotion""" + + threat = False + for effect in action.effect: + if self.is_a_threat(causal_link[1], effect): + threat = True + break + + if action != causal_link[0] and action != causal_link[2] and threat: + # try promotion + new_constraints = set(constraints) + new_constraints.add((action, causal_link[0])) + if not self.cyclic(new_constraints): + constraints = self.add_const((action, causal_link[0]), constraints) + else: + # try demotion + new_constraints = set(constraints) + new_constraints.add((causal_link[2], action)) + if not self.cyclic(new_constraints): + constraints = self.add_const((causal_link[2], action), constraints) + else: + # both promotion and demotion fail + print('Unable to resolve a threat caused by', action, 'onto', causal_link) + return + return constraints + + def convert(self, constraints): + """Convert constraints into a dict of Action: set() orderings""" + + graph = dict() + for constraint in constraints: + if constraint[0] in graph: + graph[constraint[0]].add(constraint[1]) + else: + graph[constraint[0]] = set() + graph[constraint[0]].add(constraint[1]) + return graph + + def toposort(self, graph): + """Generate topological ordering of constraints""" + + if len(graph) == 0: + return + + graph = graph.copy() + + for k, v in graph.items(): + v.discard(k) + + extra_elements_in_dependencies = _reduce(set.union, graph.values()) - set(graph.keys()) + + graph.update({element:set() for element in extra_elements_in_dependencies}) + while True: + ordered = set(element for element, dependency in graph.items() if len(dependency) == 0) + if not ordered: + break + yield ordered + graph = {element: (dependency - ordered) for element, dependency in graph.items() if element not in ordered} + if len(graph) != 0: + raise ValueError('The graph is not acyclic and cannot be linearly ordered') + + def display_plan(self): + """Display causal links, constraints and the plan""" + + print('Causal Links') + for causal_link in self.causal_links: + print(causal_link) + + print('\nConstraints') + for constraint in self.constraints: + print(constraint[0], '<', constraint[1]) + + print('\nPartial Order Plan') + print(list(reversed(list(self.toposort(self.convert(self.constraints)))))) + + def execute(self, display=True): + """Execute the algorithm""" + + step = 1 + self.tries = 1 + while len(self.agenda) > 0: + step += 1 + # select from Agenda + try: + G, act1, possible_actions = self.find_open_precondition() + except IndexError: + print('Probably Wrong') + break + + act0 = possible_actions[0] + # remove from Agenda + self.agenda.remove((G, act1)) + + # For actions with variable number of arguments, use least commitment principle + # act0_temp, bindings = self.find_action_for_precondition(G) + # act0 = self.generate_action_object(act0_temp, bindings) + + # Actions = Actions U {act0} + self.actions.add(act0) + + # Constraints = add_const(start < act0, Constraints) + self.constraints = self.add_const((self.start, act0), self.constraints) + + # for each CL E CausalLinks do + # Constraints = protect(CL, act0, Constraints) + for causal_link in self.causal_links: + self.constraints = self.protect(causal_link, act0, self.constraints) + + # Agenda = Agenda U {: P is a precondition of act0} + for precondition in act0.precond: + self.agenda.add((precondition, act0)) + + # Constraints = add_const(act0 < act1, Constraints) + self.constraints = self.add_const((act0, act1), self.constraints) + + # CausalLinks U {} + if (act0, G, act1) not in self.causal_links: + self.causal_links.append((act0, G, act1)) + + # for each A E Actions do + # Constraints = protect(, A, Constraints) + for action in self.actions: + self.constraints = self.protect((act0, G, act1), action, self.constraints) + + if step > 200: + print('Couldn\'t find a solution') + return None, None + + if display: + self.display_plan() + else: + return self.constraints, self.causal_links + + def spare_tire_graphplan(): """Solves the spare tire problem using GraphPlan""" return GraphPlan(spare_tire()).execute() @@ -597,6 +960,10 @@ def socks_and_shoes_graphplan(): """Solves the socks and shoes problem using GraphpPlan""" return GraphPlan(socks_and_shoes()).execute() +def simple_blocks_world_graphplan(): + """Solves the simple blocks world problem""" + return GraphPlan(simple_blocks_world()).execute() + class HLA(Action): """ From a68cd7a9e82e0433fef8037c9f184d684ec6381f Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 00:04:06 +0530 Subject: [PATCH 02/19] Added doctests --- planning.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 161 insertions(+), 11 deletions(-) diff --git a/planning.py b/planning.py index 04e9aefc3..d4b87b38b 100644 --- a/planning.py +++ b/planning.py @@ -153,7 +153,29 @@ def act(self, kb, args): def air_cargo(): - """Air cargo problem""" + """ + [Figure 10.1] AIR-CARGO-PROBLEM + + An air-cargo shipment problem for delivering cargo to different locations, + given the starting location and airplanes. + + Example: + >>> from planning import * + >>> ac = air_cargo() + >>> ac.goal_test() + False + >>> ac.act(expr('Load(C2, P2, JFK)')) + >>> ac.act(expr('Load(C1, P2, SFO)')) + >>> ac.act(expr('Fly(P1, SFO, JFK)')) + >>> ac.act(expr('Fly(P2, JFK, SFO)')) + >>> ac.act(expr('Unload(C2, P2, SFO)')) + >>> ac.goal_test() + False + >>> ac.act(expr('Unload(C1, P1, JFK)')) + >>> ac.goal_test() + True + >>> + """ 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)', @@ -169,7 +191,25 @@ def air_cargo(): def spare_tire(): - """Spare tire problem""" + """[Figure 10.2] SPARE-TIRE-PROBLEM + + A problem involving changing the flat tire of a car + with a spare tire from the trunk. + + Example: + >>> from planning import * + >>> st = spare_tire() + >>> st.goal_test() + False + >>> st.act(expr('Remove(Spare, Trunk)')) + >>> st.act(expr('Remove(Flat, Axle)')) + >>> st.goal_test() + False + >>> st.act(expr('PutOn(Spare, Axle)')) + >>> st.goal_test() + True + >>> + """ return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)', goals='At(Spare, Axle) & At(Flat, Ground)', @@ -186,7 +226,26 @@ def spare_tire(): def three_block_tower(): - """Sussman Anomaly problem""" + """ + [Figure 10.3] THREE-BLOCK-TOWER + + A blocks-world problem of stacking three blocks in a certain configuration, + also known as the Sussman Anomaly. + + Example: + >>> from planning import * + >>> tbt = three_block_tower() + >>> tbt.goal_test() + False + >>> tbt.act(expr('MoveToTable(C, A)')) + >>> tbt.act(expr('Move(B, Table, C)')) + >>> tbt.goal_test() + False + >>> tbt.act(expr('Move(A, Table, B)')) + >>> tbt.goal_test() + True + >>> + """ 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)', @@ -199,7 +258,25 @@ def three_block_tower(): def simple_blocks_world(): - """Simplified blocks world""" + """ + SIMPLE-BLOCKS-WORLD + + A simplified definition of the Sussman Anomaly problem. + + Example: + >>> from planning import * + >>> sbw = simple_blocks_world() + >>> sbw.goal_test() + False + >>> sbw.act(expr('ToTable(A, B)')) + >>> sbw.act(expr('FromTable(B, A)')) + >>> sbw.goal_test() + False + >>> sbw.act(expr('FromTable(C, B)')) + >>> sbw.goal_test() + True + >>> + """ return PDDL(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)', goals='On(B, A) & On(C, B)', @@ -212,7 +289,26 @@ def simple_blocks_world(): def have_cake_and_eat_cake_too(): - """Cake problem""" + """ + [Figure 10.7] CAKE-PROBLEM + + A problem where we begin with a cake and want to + reach the state of having a cake and having eaten a cake. + The possible actions include baking a cake and eating a cake. + + Example: + >>> from planning import * + >>> cp = have_cake_and_eat_cake_too() + >>> cp.goal_test() + False + >>> cp.act(expr('Bake(Cake)')) + >>> cp.goal_test() + False + >>> cp.act(expr('Eat(Cake)')) + >>> cp.goal_test() + True + >>> + """ return PDDL(init='Have(Cake)', goals='Have(Cake) & Eaten(Cake)', @@ -225,7 +321,27 @@ def have_cake_and_eat_cake_too(): def shopping_problem(): - """Shopping problem""" + """ + SHOPPING-PROBLEM + + A problem of acquiring some items given their availability at certain stores. + + Example: + >>> from planning import * + >>> sp = shopping_problem() + >>> sp.goal_test() + False + >>> sp.act(expr('Go(Home, HW)')) + >>> sp.act(expr('Buy(Drill, HW)')) + >>> sp.act(expr('Go(HW, SM)')) + >>> sp.act(expr('Buy(Banana, SM)')) + >>> sp.goal_test() + False + >>> sp.act(expr('Buy(Milk, SM)')) + >>> sp.goal_test() + True + >>> + """ return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)', goals='Have(Milk) & Have(Banana) & Have(Drill)', @@ -238,7 +354,26 @@ def shopping_problem(): def socks_and_shoes(): - """Socks and shoes problem""" + """ + SOCKS-AND-SHOES-PROBLEM + + A task of wearing socks and shoes on both feet + + Example: + >>> from planning import * + >>> ss = socks_and_shoes() + >>> ss.goal_test() + False + >>> ss.act(expr('RightSock')) + >>> ss.act(expr('RightShoe')) + >>> ss.act(expr('LeftSock')) + >>> ss.goal_test() + False + >>> ss.act(expr('LeftShoe')) + >>> ss.goal_test() + True + >>> + """ return PDDL(init='', goals='RightShoeOn & LeftShoeOn', @@ -592,10 +727,25 @@ def linearize(solution): ''' -[10.13 PartialOrderPlanner: Partially ordered plans are created by a search through the space of plans +[Section 10.13] PARTIAL-ORDER-PLANNER + +Partially ordered plans are created by a search through the space of plans rather than a search through the state space. It views planning as a refinement of partially ordered plans. A partially ordered plan is defined by a set of actions and a set of constraints of the form A < B, -which denotes that action A has to be performed before action B.] +which denotes that action A has to be performed before action B. +To summarize the working of a partial order planner, +1. An open precondition is selected (a sub-goal that we want to achieve). +2. An action that fulfils the open precondition is chosen. +3. Temporal constraints are updated. +4. Existing causal links are protected. Protection is a method that checks if the causal links conflict + and if they do, temporal constraints are added to fix the threats. +5. The set of open preconditions is updated. +6. Temporal constraints of the selected action and the next action are established. +7. A new causal link is added between the selected action and the owner of the open precondition. +8. The set of new causal links is checked for threats and if found, the threat is removed by either promotion or demotion. + If promotion or demotion is unable to solve the problem, the planning problem cannot be solved with the current sequence of actions + or it may not be solvable at all. +9. These steps are repeated until the set of open preconditions is empty. ''' class PartialOrderPlanner: @@ -829,7 +979,7 @@ def protect(self, causal_link, action, constraints): return constraints def convert(self, constraints): - """Convert constraints into a dict of Action: set() orderings""" + """Convert constraints into a dict of Action to set orderings""" graph = dict() for constraint in constraints: @@ -1146,7 +1296,7 @@ def result(problem, action): def job_shop_problem(): """ - [figure 11.1] JOB-SHOP-PROBLEM + [Figure 11.1] JOB-SHOP-PROBLEM A job-shop scheduling problem for assembling two cars, with resource and ordering constraints. From 8843d6f3a9b45a8350b534030117f59b0640f7e5 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 00:24:21 +0530 Subject: [PATCH 03/19] Fix doctests --- planning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/planning.py b/planning.py index d4b87b38b..b44572c93 100644 --- a/planning.py +++ b/planning.py @@ -165,7 +165,7 @@ def air_cargo(): >>> ac.goal_test() False >>> ac.act(expr('Load(C2, P2, JFK)')) - >>> ac.act(expr('Load(C1, P2, SFO)')) + >>> ac.act(expr('Load(C1, P1, SFO)')) >>> ac.act(expr('Fly(P1, SFO, JFK)')) >>> ac.act(expr('Fly(P2, JFK, SFO)')) >>> ac.act(expr('Unload(C2, P2, SFO)')) @@ -301,10 +301,10 @@ def have_cake_and_eat_cake_too(): >>> cp = have_cake_and_eat_cake_too() >>> cp.goal_test() False - >>> cp.act(expr('Bake(Cake)')) + >>> cp.act(expr('Eat(Cake)')) >>> cp.goal_test() False - >>> cp.act(expr('Eat(Cake)')) + >>> cp.act(expr('Bake(Cake)')) >>> cp.goal_test() True >>> From 29698366d82abe5825c605a9963250202e367b87 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 01:09:38 +0530 Subject: [PATCH 04/19] Added tests for PartialOrderPlanner methods --- tests/test_planning.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_planning.py b/tests/test_planning.py index 641a2eeca..787e591e8 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -195,6 +195,50 @@ def test_total_order_planner(): assert TotalOrderPlanner(ss).execute() in possible_solutions +def test_expand_actions(): + assert len(PartialOrderPlanner(spare_tire()).expand_actions()) == 16 + assert len(PartialOrderPlanner(air_cargo()).expand_actions()) == 360 + assert len(PartialOrderPlanner(have_cake_and_eat_cake_too()).expand_actions()) == 2 + assert len(PartialOrderPlanner(socks_and_shoes()).expand_actions()) == 4 + assert len(PartialOrderPlanner(simple_blocks_world()).expand_actions()) == 12 + assert len(PartialOrderPlanner(three_block_tower()).expand_actions()) == 36 + + +def test_find_open_precondition(): + st = spare_tire() + pop = PartialOrderPlanner(st) + assert pop.find_open_precondition()[0] == expr('At(Spare, Axle)') + assert pop.find_open_precondition()[1] == pop.finish + assert pop.find_open_precondition()[2][0].name == 'PutOn' + + ss = socks_and_shoes() + pop = PartialOrderPlanner(ss) + assert (pop.find_open_precondition()[0] == expr('LeftShoeOn') and pop.find_open_precondition()[2][0].name == 'LeftShoe') or (pop.find_open_precondition()[0] == expr('RightShoeOn') and pop.find_open_precondition()[2][0].name == 'RightShoe') + assert pop.find_open_precondition()[1] == pop.finish + + cp = have_cake_and_eat_cake_too() + pop = PartialOrderPlanner(cp) + assert pop.find_open_precondition()[0] == expr('Eaten(Cake)') + assert pop.find_open_precondition()[1] == pop.finish + assert pop.find_open_precondition()[2][0].name == 'Eat' + + +def test_cyclic(): + st = spare_tire() + pop = PartialOrderPlanner(st) + graph = [('a', 'b'), ('a', 'c'), ('b', 'c'), ('b', 'd'), ('d', 'e'), ('e', 'c')] + assert not pop.cyclic(graph) + + graph = [('a', 'b'), ('a', 'c'), ('b', 'c'), ('b', 'd'), ('d', 'e'), ('e', 'c'), ('e', 'b')] + assert pop.cyclic(graph) + + graph = [('a', 'b'), ('a', 'c'), ('b', 'c'), ('b', 'd'), ('d', 'e'), ('e', 'c'), ('b', 'e'), ('a', 'e')] + assert not pop.cyclic(graph) + + graph = [('a', 'b'), ('a', 'c'), ('b', 'c'), ('b', 'd'), ('d', 'e'), ('e', 'c'), ('e', 'b'), ('b', 'e'), ('a', 'e')] + assert pop.cyclic(graph) + + # def test_double_tennis(): # p = double_tennis_problem # assert p.goal_test() is False From 3d18a1ee122b62eb9886bb7f33a4504c57fdcdf3 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 01:25:26 +0530 Subject: [PATCH 05/19] Added test for PartialOrderPlanner --- tests/test_planning.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_planning.py b/tests/test_planning.py index 787e591e8..4a2ce1fa6 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -239,6 +239,17 @@ def test_cyclic(): assert pop.cyclic(graph) +def test_partial_order_planner(): + ss = socks_and_shoes() + pop = PartialOrderPlanner(ss) + constraints, causal_links = pop.execute(display=False) + plan = list(reversed(list(pop.toposort(pop.convert(pop.constraints))))) + assert list(plan[0])[0].name == 'Start' + assert (list(plan[1])[0].name == 'LeftSock' and list(plan[1])[1].name == 'RightSock') or (list(plan[1])[0].name == 'RightSock' and list(plan[1])[1].name == 'LeftSock') + assert (list(plan[2])[0].name == 'LeftShoe' and list(plan[2])[1].name == 'RightShoe') or (list(plan[2])[0].name == 'RightShoe' and list(plan[2])[1].name == 'LeftShoe') + assert list(plan[3])[0].name == 'Finish' + + # def test_double_tennis(): # p = double_tennis_problem # assert p.goal_test() is False From 7eebea8c34fa8bf05c922575dad291c85a5c3290 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 07:34:10 +0530 Subject: [PATCH 06/19] Rerun planning.ipynb --- planning.ipynb | 245 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 178 insertions(+), 67 deletions(-) diff --git a/planning.ipynb b/planning.ipynb index fd21a6e88..a79d45222 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -174,20 +174,32 @@ "\n", " def __init__(self, init, goals, actions):\n", " self.init = self.convert(init)\n", - " self.goals = expr(goals)\n", + " self.goals = self.convert(goals)\n", " self.actions = actions\n", "\n", - " def convert(self, init):\n", + " def convert(self, clauses):\n", " """Converts strings into exprs"""\n", + " if not isinstance(clauses, Expr):\n", + " if len(clauses) > 0:\n", + " clauses = expr(clauses)\n", + " else:\n", + " clauses = []\n", " try:\n", - " init = conjuncts(expr(init))\n", + " clauses = conjuncts(clauses)\n", " except AttributeError:\n", - " init = expr(init)\n", - " return init\n", + " clauses = clauses\n", + "\n", + " new_clauses = []\n", + " for clause in clauses:\n", + " if clause.op == '~':\n", + " new_clauses.append(expr('Not' + str(clause.args[0])))\n", + " else:\n", + " new_clauses.append(clause)\n", + " return new_clauses\n", "\n", " def goal_test(self):\n", " """Checks if the goals have been reached"""\n", - " return all(goal in self.init for goal in conjuncts(self.goals))\n", + " return all(goal in self.init for goal in self.goals)\n", "\n", " def act(self, action):\n", " """\n", @@ -361,34 +373,38 @@ " """\n", "\n", " def __init__(self, action, precond, effect):\n", - " action = expr(action)\n", + " if isinstance(action, str):\n", + " action = expr(action)\n", " self.name = action.op\n", " self.args = action.args\n", - " self.precond, self.effect = self.convert(precond, effect)\n", + " self.precond = self.convert(precond)\n", + " self.effect = self.convert(effect)\n", "\n", " def __call__(self, kb, args):\n", " return self.act(kb, args)\n", "\n", - " def convert(self, precond, effect):\n", + " def __repr__(self):\n", + " return '{}({})'.format(self.__class__.__name__, Expr(self.name, *self.args))\n", + "\n", + " def convert(self, clauses):\n", " """Converts strings into Exprs"""\n", + " if isinstance(clauses, Expr):\n", + " clauses = conjuncts(clauses)\n", + " for i in range(len(clauses)):\n", + " if clauses[i].op == '~':\n", + " clauses[i] = expr('Not' + str(clauses[i].args[0]))\n", "\n", - " precond = precond.replace('~', 'Not')\n", - " if len(precond) > 0:\n", - " precond = expr(precond)\n", - " effect = effect.replace('~', 'Not')\n", - " if len(effect) > 0:\n", - " effect = expr(effect)\n", + " elif isinstance(clauses, str):\n", + " clauses = clauses.replace('~', 'Not')\n", + " if len(clauses) > 0:\n", + " clauses = expr(clauses)\n", "\n", - " try:\n", - " precond = conjuncts(precond)\n", - " except AttributeError:\n", - " pass\n", - " try:\n", - " effect = conjuncts(effect)\n", - " except AttributeError:\n", - " pass\n", + " try:\n", + " clauses = conjuncts(clauses)\n", + " except AttributeError:\n", + " pass\n", "\n", - " return precond, effect\n", + " return clauses\n", "\n", " def substitute(self, e, args):\n", " """Replaces variables in expression with their respective Propositional symbol"""\n", @@ -405,7 +421,6 @@ "\n", " if isinstance(kb, list):\n", " kb = FolKB(kb)\n", - "\n", " for clause in self.precond:\n", " if self.substitute(clause, args) not in kb.clauses:\n", " return False\n", @@ -793,12 +808,34 @@ "

\n", "\n", "
def air_cargo():\n",
-       "    """Air cargo problem"""\n",
+       "    """\n",
+       "    [Figure 10.1] AIR-CARGO-PROBLEM\n",
+       "\n",
+       "    An air-cargo shipment problem for delivering cargo to different locations,\n",
+       "    given the starting location and airplanes.\n",
        "\n",
-       "    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)',\n",
-       "                goals='At(C1, JFK) & At(C2, SFO)', \n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> ac = air_cargo()\n",
+       "    >>> ac.goal_test()\n",
+       "    False\n",
+       "    >>> ac.act(expr('Load(C2, P2, JFK)'))\n",
+       "    >>> ac.act(expr('Load(C1, P1, SFO)'))\n",
+       "    >>> ac.act(expr('Fly(P1, SFO, JFK)'))\n",
+       "    >>> ac.act(expr('Fly(P2, JFK, SFO)'))\n",
+       "    >>> ac.act(expr('Unload(C2, P2, SFO)'))\n",
+       "    >>> ac.goal_test()\n",
+       "    False\n",
+       "    >>> ac.act(expr('Unload(C1, P1, JFK)'))\n",
+       "    >>> ac.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
+       "\n",
+       "    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)', \n",
+       "                goals='At(C1, JFK) & At(C2, SFO)',\n",
        "                actions=[Action('Load(c, p, a)', \n",
-       "                                precond='At(c, a) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)', \n",
+       "                                precond='At(c, a) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',\n",
        "                                effect='In(c, p) & ~At(c, a)'),\n",
        "                         Action('Unload(c, p, a)',\n",
        "                                precond='In(c, p) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',\n",
@@ -1060,7 +1097,25 @@
        "

\n", "\n", "
def spare_tire():\n",
-       "    """Spare tire problem"""\n",
+       "    """[Figure 10.2] SPARE-TIRE-PROBLEM\n",
+       "\n",
+       "    A problem involving changing the flat tire of a car\n",
+       "    with a spare tire from the trunk.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> st = spare_tire()\n",
+       "    >>> st.goal_test()\n",
+       "    False\n",
+       "    >>> st.act(expr('Remove(Spare, Trunk)'))\n",
+       "    >>> st.act(expr('Remove(Flat, Axle)'))\n",
+       "    >>> st.goal_test()\n",
+       "    False\n",
+       "    >>> st.act(expr('PutOn(Spare, Axle)'))\n",
+       "    >>> st.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
        "\n",
        "    return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',\n",
        "                goals='At(Spare, Axle) & At(Flat, Ground)',\n",
@@ -1369,7 +1424,26 @@
        "

\n", "\n", "
def three_block_tower():\n",
-       "    """Sussman Anomaly problem"""\n",
+       "    """\n",
+       "    [Figure 10.3] THREE-BLOCK-TOWER\n",
+       "\n",
+       "    A blocks-world problem of stacking three blocks in a certain configuration,\n",
+       "    also known as the Sussman Anomaly.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> tbt = three_block_tower()\n",
+       "    >>> tbt.goal_test()\n",
+       "    False\n",
+       "    >>> tbt.act(expr('MoveToTable(C, A)'))\n",
+       "    >>> tbt.act(expr('Move(B, Table, C)'))\n",
+       "    >>> tbt.goal_test()\n",
+       "    False\n",
+       "    >>> tbt.act(expr('Move(A, Table, B)'))\n",
+       "    >>> tbt.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
        "\n",
        "    return PDDL(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',\n",
        "                goals='On(A, B) & On(B, C)',\n",
@@ -1620,7 +1694,27 @@
        "

\n", "\n", "
def shopping_problem():\n",
-       "    """Shopping problem"""\n",
+       "    """\n",
+       "    SHOPPING-PROBLEM\n",
+       "\n",
+       "    A problem of acquiring some items given their availability at certain stores.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> sp = shopping_problem()\n",
+       "    >>> sp.goal_test()\n",
+       "    False\n",
+       "    >>> sp.act(expr('Go(Home, HW)'))\n",
+       "    >>> sp.act(expr('Buy(Drill, HW)'))\n",
+       "    >>> sp.act(expr('Go(HW, SM)'))\n",
+       "    >>> sp.act(expr('Buy(Banana, SM)'))\n",
+       "    >>> sp.goal_test()\n",
+       "    False\n",
+       "    >>> sp.act(expr('Buy(Milk, SM)'))\n",
+       "    >>> sp.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
        "\n",
        "    return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',\n",
        "                goals='Have(Milk) & Have(Banana) & Have(Drill)', \n",
@@ -1770,7 +1864,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## Have Cake and Eat Cake Too"
+    "## Cake Problem"
    ]
   },
   {
@@ -1876,7 +1970,26 @@
        "

\n", "\n", "
def have_cake_and_eat_cake_too():\n",
-       "    """Cake problem"""\n",
+       "    """\n",
+       "    [Figure 10.7] CAKE-PROBLEM\n",
+       "\n",
+       "    A problem where we begin with a cake and want to \n",
+       "    reach the state of having a cake and having eaten a cake.\n",
+       "    The possible actions include baking a cake and eating a cake.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> cp = have_cake_and_eat_cake_too()\n",
+       "    >>> cp.goal_test()\n",
+       "    False\n",
+       "    >>> cp.act(expr('Eat(Cake)'))\n",
+       "    >>> cp.goal_test()\n",
+       "    False\n",
+       "    >>> cp.act(expr('Bake(Cake)'))\n",
+       "    >>> cp.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
        "\n",
        "    return PDDL(init='Have(Cake)',\n",
        "                goals='Have(Cake) & Eaten(Cake)',\n",
@@ -2034,7 +2147,7 @@
       "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[1;31mException\u001b[0m                                 Traceback (most recent call last)",
       "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      6\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[1;32min\u001b[0m \u001b[0msolution\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 7\u001b[1;33m     \u001b[0mcakeProblem\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mact\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
-      "\u001b[1;32m~\\Documents\\Python\\Aima\\aima-python\\planning.py\u001b[0m in \u001b[0;36mact\u001b[1;34m(self, action)\u001b[0m\n\u001b[0;32m     44\u001b[0m             \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' not found\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction_name\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     45\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 46\u001b[1;33m             \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' pre-conditions not satisfied\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     47\u001b[0m         \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     48\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n",
+      "\u001b[1;32m~\\Documents\\Python\\Data Science\\Machine Learning\\Aima\\planning.py\u001b[0m in \u001b[0;36mact\u001b[1;34m(self, action)\u001b[0m\n\u001b[0;32m     58\u001b[0m             \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' not found\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction_name\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     59\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 60\u001b[1;33m             \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' pre-conditions not satisfied\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     61\u001b[0m         \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     62\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n",
       "\u001b[1;31mException\u001b[0m: Action 'Bake(Cake)' pre-conditions not satisfied"
      ]
     }
@@ -2714,6 +2827,22 @@
        "            solution[num] = item\n",
        "\n",
        "        return solution\n",
+       "\n",
+       "    def goal_test(self, kb):\n",
+       "        return all(kb.ask(q) is not False for q in self.graph.pddl.goals)\n",
+       "\n",
+       "    def execute(self):\n",
+       "        """Executes the GraphPlan algorithm for the given problem"""\n",
+       "\n",
+       "        while True:\n",
+       "            self.graph.expand_graph()\n",
+       "            if (self.goal_test(self.graph.levels[-1].kb) and self.graph.non_mutex_goals(self.graph.pddl.goals, -1)):\n",
+       "                solution = self.extract_solution(self.graph.pddl.goals, -1)\n",
+       "                if solution:\n",
+       "                    return solution\n",
+       "            \n",
+       "            if len(self.graph.levels) >= 2 and self.check_leveloff():\n",
+       "                return None\n",
        "
\n", "\n", "\n" @@ -2765,8 +2894,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Air cargo problem:\n", - "
\n", + "#### Air cargo problem\n", "In accordance with the summary above, we have defined a helper function to carry out `GraphPlan` on the `air_cargo` problem.\n", "The function is pretty straightforward.\n", "Let's have a look." @@ -2868,24 +2996,7 @@ "\n", "
def air_cargo_graphplan():\n",
        "    """Solves the air cargo problem using GraphPlan"""\n",
-       "\n",
-       "    pddl = air_cargo()\n",
-       "    graphplan = GraphPlan(pddl)\n",
-       "\n",
-       "    def goal_test(kb, goals):\n",
-       "        return all(kb.ask(q) is not False for q in goals)\n",
-       "\n",
-       "    goals = expr('At(C1, JFK), At(C2, SFO)')\n",
-       "\n",
-       "    while True:\n",
-       "        if (goal_test(graphplan.graph.levels[-1].kb, goals) and graphplan.graph.non_mutex_goals(goals, -1)):\n",
-       "            solution = graphplan.extract_solution(goals, -1)\n",
-       "            if solution:\n",
-       "                return solution\n",
-       "\n",
-       "        graphplan.graph.expand_graph()\n",
-       "        if len(graphplan.graph.levels) >= 2 and graphplan.check_leveloff():\n",
-       "            return None\n",
+       "    return GraphPlan(air_cargo()).execute()\n",
        "
\n", "\n", "\n" @@ -2917,17 +3028,17 @@ { "data": { "text/plain": [ - "[[[PCargo(C2),\n", + "[[[PAirport(JFK),\n", " Load(C2, P2, JFK),\n", + " Load(C1, P1, SFO)],\n", + " PPlane(P1),\n", " PPlane(P2),\n", - " Load(C1, P1, SFO),\n", - " Fly(P1, SFO, JFK),\n", " PAirport(SFO),\n", - " PAirport(JFK),\n", - " PPlane(P1),\n", + " PCargo(C2),\n", + " Fly(P2, JFK, SFO),\n", " PCargo(C1),\n", - " Fly(P2, JFK, SFO)],\n", - " [Unload(C2, P2, SFO), Unload(C1, P1, JFK)]]]" + " Fly(P1, SFO, JFK),\n", + " [Unload(C1, P1, JFK), Unload(C2, P2, SFO)]]]" ] }, "execution_count": 44, @@ -2948,7 +3059,7 @@ "The solution is separated into lists for each level.\n", "The actions prefixed with a 'P' are persistence actions and can be ignored.\n", "They simply carry certain states forward.\n", - "We have another helper function `linearize` that presents the solution in a more readable format, much like a total-order planner." + "We have another helper function `linearize` that presents the solution in a more readable format, much like a total-order planner, but it is _not_ a total-order planner." ] }, { @@ -2961,10 +3072,10 @@ "text/plain": [ "[Load(C2, P2, JFK),\n", " Load(C1, P1, SFO),\n", - " Fly(P1, SFO, JFK),\n", " Fly(P2, JFK, SFO),\n", - " Unload(C2, P2, SFO),\n", - " Unload(C1, P1, JFK)]" + " Fly(P1, SFO, JFK),\n", + " Unload(C1, P1, JFK),\n", + " Unload(C2, P2, SFO)]" ] }, "execution_count": 45, @@ -2995,7 +3106,7 @@ { "data": { "text/plain": [ - "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" ] }, "execution_count": 46, From 30d38a0d2a62cf3091153378abace8e6a354840c Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 08:56:30 +0530 Subject: [PATCH 07/19] Added notebook section for TotalOrderPlanner --- planning.ipynb | 1438 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 1152 insertions(+), 286 deletions(-) diff --git a/planning.ipynb b/planning.ipynb index a79d45222..7dca111a2 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -51,9 +51,12 @@ " * Spare tire problem\n", " * Three block tower problem\n", " * Shopping Problem\n", + " * Socks and shoes problem\n", " * Cake problem\n", "- Solving Planning Problems\n", - " * GraphPlan" + " * GraphPlan\n", + " * TotalOrderPlanner\n", + " * PartiaOrderPlanner" ] }, { @@ -1587,16 +1590,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Shopping Problem" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This problem requires us to acquire a carton of milk, a banana and a drill.\n", - "Initially, we start from home and it is known to us that milk and bananas are available in the supermarket and the hardware store sells drills.\n", - "Let's take a look at the definition of the `shopping_problem` in the module." + "The `three_block_tower` problem can also be defined in simpler terms using just two actions `ToTable(x, y)` and `FromTable(x, y)`.\n", + "The underlying problem remains the same however, stacking up three blocks in a certain configuration given a particular starting state.\n", + "Let's have a look at the alternative definition." ] }, { @@ -1693,37 +1689,35 @@ "\n", "

\n", "\n", - "
def shopping_problem():\n",
+       "
def simple_blocks_world():\n",
        "    """\n",
-       "    SHOPPING-PROBLEM\n",
+       "    SIMPLE-BLOCKS-WORLD\n",
        "\n",
-       "    A problem of acquiring some items given their availability at certain stores.\n",
+       "    A simplified definition of the Sussman Anomaly problem.\n",
        "\n",
        "    Example:\n",
        "    >>> from planning import *\n",
-       "    >>> sp = shopping_problem()\n",
-       "    >>> sp.goal_test()\n",
+       "    >>> sbw = simple_blocks_world()\n",
+       "    >>> sbw.goal_test()\n",
        "    False\n",
-       "    >>> sp.act(expr('Go(Home, HW)'))\n",
-       "    >>> sp.act(expr('Buy(Drill, HW)'))\n",
-       "    >>> sp.act(expr('Go(HW, SM)'))\n",
-       "    >>> sp.act(expr('Buy(Banana, SM)'))\n",
-       "    >>> sp.goal_test()\n",
+       "    >>> sbw.act(expr('ToTable(A, B)'))\n",
+       "    >>> sbw.act(expr('FromTable(B, A)'))\n",
+       "    >>> sbw.goal_test()\n",
        "    False\n",
-       "    >>> sp.act(expr('Buy(Milk, SM)'))\n",
-       "    >>> sp.goal_test()\n",
+       "    >>> sbw.act(expr('FromTable(C, B)'))\n",
+       "    >>> sbw.goal_test()\n",
        "    True\n",
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',\n",
-       "                goals='Have(Milk) & Have(Banana) & Have(Drill)', \n",
-       "                actions=[Action('Buy(x, store)',\n",
-       "                                precond='At(store) & Sells(store, x)',\n",
-       "                                effect='Have(x)'),\n",
-       "                         Action('Go(x, y)',\n",
-       "                                precond='At(x)',\n",
-       "                                effect='At(y) & ~At(x)')])\n",
+       "    return PDDL(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)',\n",
+       "                goals='On(B, A) & On(C, B)',\n",
+       "                actions=[Action('ToTable(x, y)',\n",
+       "                                precond='On(x, y) & Clear(x)',\n",
+       "                                effect='~On(x, y) & Clear(y) & OnTable(x)'),\n",
+       "                         Action('FromTable(y, x)',\n",
+       "                                precond='OnTable(y) & Clear(y) & Clear(x)',\n",
+       "                                effect='~OnTable(y) & ~Clear(x) & On(y, x)')])\n",
        "
\n", "\n", "\n" @@ -1737,20 +1731,26 @@ } ], "source": [ - "psource(shopping_problem)" + "psource(simple_blocks_world)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**At(x):** Indicates that we are currently at **'x'** where **'x'** can be Home, SM (supermarket) or HW (Hardware store).\n", + "**On(x, y):** The block **'x'** is on **'y'**. Both **'x'** and **'y'** have to be blocks.\n", "\n", - "**~At(x):** Indicates that we are currently _not_ at **'x'**.\n", + "**~On(x, y):** The block **'x'** is _not_ on **'y'**. Both **'x'** and **'y'** have to be blocks.\n", "\n", - "**Sells(s, x):** Indicates that item **'x'** can be bought from store **'s'**.\n", + "**OnTable(x):** The block **'x'** is on the table.\n", "\n", - "**Have(x):** Indicates that we possess the item **'x'**." + "**~OnTable(x):** The block **'x'** is _not_ on the table.\n", + "\n", + "**Clear(x):** To indicate that there is nothing on **'x'** and it is free to be moved around.\n", + "\n", + "**~Clear(x):** To indicate that there is something on **'x'** and it cannot be moved.\n", + "\n", + "Let's now define a `simple_blocks_world` prolem." ] }, { @@ -1761,14 +1761,14 @@ }, "outputs": [], "source": [ - "shoppingProblem = shopping_problem()" + "simpleBlocksWorld = simple_blocks_world()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's first check whether the goal state Have(Milk), Have(Banana), Have(Drill) is reached or not." + "Before taking any actions, we will see if `simple_bw` has reached its goal." ] }, { @@ -1777,60 +1777,54 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "False\n" - ] + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "print(shoppingProblem.goal_test())" + "simpleBlocksWorld.goal_test()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's look at the possible actions\n", + "As we can see, it hasn't completed the goal. \n", + "We now define a sequence of actions that can stack three blocks in the required order. \n", + "The actions are then carried out on the `simple_bw` PDDL.\n", "\n", - "**Buy(x, store):** Buy an item **'x'** from a **'store'** given that the **'store'** sells **'x'**.\n", + "The actions available to us are the following: MoveToTable, Move\n", "\n", - "**Go(x, y):** Go to destination **'y'** starting from source **'x'**." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now define a valid solution that will help us reach the goal.\n", - "The sequence of actions will then be carried out onto the `shoppingProblem` PDDL." + "**ToTable(x, y): ** Move box **'x'** stacked on **'y'** to the table, given that box **'y'** is clear.\n", + "\n", + "**FromTable(x, y): ** Move box **'x'** from wherever it is, to the top of **'y'**, given that both **'x'** and **'y'** are clear.\n" ] }, { "cell_type": "code", "execution_count": 32, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "solution = [expr('Go(Home, SM)'),\n", - " expr('Buy(Milk, SM)'),\n", - " expr('Buy(Banana, SM)'),\n", - " expr('Go(SM, HW)'),\n", - " expr('Buy(Drill, HW)')]\n", + "solution = [expr('ToTable(A, B)'),\n", + " expr('FromTable(B, A)'),\n", + " expr('FromTable(C, B)')]\n", "\n", "for action in solution:\n", - " shoppingProblem.act(action)" + " simpleBlocksWorld.act(action)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We have taken the steps required to acquire all the stuff we need. \n", - "Let's see if we have reached our goal." + "As the `three_block_tower` has taken all the steps it needed in order to achieve the goal, we can now check if it has acheived its goal." ] }, { @@ -1839,40 +1833,38 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] } ], "source": [ - "shoppingProblem.goal_test()" + "print(simpleBlocksWorld.goal_test())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It has now successfully achieved the goal." + "It has now successfully achieved its goal i.e, to build a stack of three blocks in the specified order." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Cake Problem" + "## Shopping Problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This problem requires us to reach the state of having a cake and having eaten a cake simlutaneously, given a single cake.\n", - "Let's first take a look at the definition of the `have_cake_and_eat_cake_too` problem in the module." + "This problem requires us to acquire a carton of milk, a banana and a drill.\n", + "Initially, we start from home and it is known to us that milk and bananas are available in the supermarket and the hardware store sells drills.\n", + "Let's take a look at the definition of the `shopping_problem` in the module." ] }, { @@ -1969,36 +1961,37 @@ "\n", "

\n", "\n", - "
def have_cake_and_eat_cake_too():\n",
+       "
def shopping_problem():\n",
        "    """\n",
-       "    [Figure 10.7] CAKE-PROBLEM\n",
+       "    SHOPPING-PROBLEM\n",
        "\n",
-       "    A problem where we begin with a cake and want to \n",
-       "    reach the state of having a cake and having eaten a cake.\n",
-       "    The possible actions include baking a cake and eating a cake.\n",
+       "    A problem of acquiring some items given their availability at certain stores.\n",
        "\n",
        "    Example:\n",
        "    >>> from planning import *\n",
-       "    >>> cp = have_cake_and_eat_cake_too()\n",
-       "    >>> cp.goal_test()\n",
+       "    >>> sp = shopping_problem()\n",
+       "    >>> sp.goal_test()\n",
        "    False\n",
-       "    >>> cp.act(expr('Eat(Cake)'))\n",
-       "    >>> cp.goal_test()\n",
+       "    >>> sp.act(expr('Go(Home, HW)'))\n",
+       "    >>> sp.act(expr('Buy(Drill, HW)'))\n",
+       "    >>> sp.act(expr('Go(HW, SM)'))\n",
+       "    >>> sp.act(expr('Buy(Banana, SM)'))\n",
+       "    >>> sp.goal_test()\n",
        "    False\n",
-       "    >>> cp.act(expr('Bake(Cake)'))\n",
-       "    >>> cp.goal_test()\n",
+       "    >>> sp.act(expr('Buy(Milk, SM)'))\n",
+       "    >>> sp.goal_test()\n",
        "    True\n",
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='Have(Cake)',\n",
-       "                goals='Have(Cake) & Eaten(Cake)',\n",
-       "                actions=[Action('Eat(Cake)',\n",
-       "                                precond='Have(Cake)',\n",
-       "                                effect='Eaten(Cake) & ~Have(Cake)'),\n",
-       "                         Action('Bake(Cake)',\n",
-       "                                precond='~Have(Cake)',\n",
-       "                                effect='Have(Cake)')])\n",
+       "    return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',\n",
+       "                goals='Have(Milk) & Have(Banana) & Have(Drill)', \n",
+       "                actions=[Action('Buy(x, store)',\n",
+       "                                precond='At(store) & Sells(store, x)',\n",
+       "                                effect='Have(x)'),\n",
+       "                         Action('Go(x, y)',\n",
+       "                                precond='At(x)',\n",
+       "                                effect='At(y) & ~At(x)')])\n",
        "
\n", "\n", "\n" @@ -2012,18 +2005,20 @@ } ], "source": [ - "psource(have_cake_and_eat_cake_too)" + "psource(shopping_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Since this problem doesn't involve variables, states can be considered similar to symbols in propositional logic.\n", + "**At(x):** Indicates that we are currently at **'x'** where **'x'** can be Home, SM (supermarket) or HW (Hardware store).\n", "\n", - "**Have(Cake):** Declares that we have a **'Cake'**.\n", + "**~At(x):** Indicates that we are currently _not_ at **'x'**.\n", "\n", - "**~Have(Cake):** Declares that we _don't_ have a **'Cake'**." + "**Sells(s, x):** Indicates that item **'x'** can be bought from store **'s'**.\n", + "\n", + "**Have(x):** Indicates that we possess the item **'x'**." ] }, { @@ -2034,14 +2029,14 @@ }, "outputs": [], "source": [ - "cakeProblem = have_cake_and_eat_cake_too()" + "shoppingProblem = shopping_problem()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "First let us check whether the goal state 'Have(Cake)' and 'Eaten(Cake)' are reached or not." + "Let's first check whether the goal state Have(Milk), Have(Banana), Have(Drill) is reached or not." ] }, { @@ -2058,26 +2053,26 @@ } ], "source": [ - "print(cakeProblem.goal_test())" + "print(shoppingProblem.goal_test())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let us look at the possible actions.\n", + "Let's look at the possible actions\n", "\n", - "**Bake(x):** To bake **' x '**.\n", + "**Buy(x, store):** Buy an item **'x'** from a **'store'** given that the **'store'** sells **'x'**.\n", "\n", - "**Eat(x):** To eat **' x '**." + "**Go(x, y):** Go to destination **'y'** starting from source **'x'**." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now define a valid solution that can help us reach the goal.\n", - "The sequence of actions will then be acted upon the `cakeProblem` PDDL." + "We now define a valid solution that will help us reach the goal.\n", + "The sequence of actions will then be carried out onto the `shoppingProblem` PDDL." ] }, { @@ -2088,18 +2083,22 @@ }, "outputs": [], "source": [ - "solution = [expr(\"Eat(Cake)\"),\n", - " expr(\"Bake(Cake)\")]\n", + "solution = [expr('Go(Home, SM)'),\n", + " expr('Buy(Milk, SM)'),\n", + " expr('Buy(Banana, SM)'),\n", + " expr('Go(SM, HW)'),\n", + " expr('Buy(Drill, HW)')]\n", "\n", "for action in solution:\n", - " cakeProblem.act(action)" + " shoppingProblem.act(action)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we have made actions to bake the cake and eat the cake. Let us check if we have reached the goal." + "We have taken the steps required to acquire all the stuff we need. \n", + "Let's see if we have reached our goal." ] }, { @@ -2108,96 +2107,629 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n" - ] + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "print(cakeProblem.goal_test())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It has now successfully achieved its goal i.e, to have and eat the cake." + "shoppingProblem.goal_test()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "One might wonder if the order of the actions matters for this problem.\n", - "Let's see for ourselves." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "Action 'Bake(Cake)' pre-conditions not satisfied", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mException\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[1;32min\u001b[0m \u001b[0msolution\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 7\u001b[1;33m \u001b[0mcakeProblem\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mact\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32m~\\Documents\\Python\\Data Science\\Machine Learning\\Aima\\planning.py\u001b[0m in \u001b[0;36mact\u001b[1;34m(self, action)\u001b[0m\n\u001b[0;32m 58\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' not found\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction_name\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 59\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 60\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' pre-conditions not satisfied\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 61\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 62\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mException\u001b[0m: Action 'Bake(Cake)' pre-conditions not satisfied" - ] - } - ], - "source": [ - "cakeProblem = have_cake_and_eat_cake_too()\n", - "\n", - "solution = [expr('Bake(Cake)'),\n", - " expr('Eat(Cake)')]\n", - "\n", - "for action in solution:\n", - " cakeProblem.act(action)" + "It has now successfully achieved the goal." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It raises an exception.\n", - "Indeed, according to the problem, we cannot bake a cake if we already have one.\n", - "In planning terms, '~Have(Cake)' is a precondition to the action 'Bake(Cake)'.\n", - "Hence, this solution is invalid." + "## Socks and Shoes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## SOLVING PLANNING PROBLEMS\n", - "----\n", - "### GRAPHPLAN\n", - "
\n", - "The GraphPlan algorithm is a popular method of solving classical planning problems.\n", - "Before we get into the details of the algorithm, let's look at a special data structure called **planning graph**, used to give better heuristic estimates and plays a key role in the GraphPlan algorithm." + "This is a simple problem of putting on a pair of socks and shoes.\n", + "The problem is defined in the module as given below." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 39, "metadata": {}, - "source": [ - "### Planning Graph\n", - "A planning graph is a directed graph organized into levels. \n", - "Each level contains information about the current state of the knowledge base and the possible state-action links to and from that level.\n", - "The first level contains the initial state with nodes representing each fluent that holds in that level.\n", - "This level has state-action links linking each state to valid actions in that state.\n", - "Each action is linked to all its preconditions and its effect states.\n", - "Based on these effects, the next level is constructed.\n", - "The next level contains similarly structured information about the next state.\n", - "In this way, the graph is expanded using state-action links till we reach a state where all the required goals hold true simultaneously.\n", - "We can say that we have reached our goal if none of the goal states in the current level are mutually exclusive.\n", + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def socks_and_shoes():\n",
+       "    """\n",
+       "    SOCKS-AND-SHOES-PROBLEM\n",
+       "\n",
+       "    A task of wearing socks and shoes on both feet\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> ss = socks_and_shoes()\n",
+       "    >>> ss.goal_test()\n",
+       "    False\n",
+       "    >>> ss.act(expr('RightSock'))\n",
+       "    >>> ss.act(expr('RightShoe'))\n",
+       "    >>> ss.act(expr('LeftSock'))\n",
+       "    >>> ss.goal_test()\n",
+       "    False\n",
+       "    >>> ss.act(expr('LeftShoe'))\n",
+       "    >>> ss.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
+       "\n",
+       "    return PDDL(init='',\n",
+       "                goals='RightShoeOn & LeftShoeOn',\n",
+       "                actions=[Action('RightShoe',\n",
+       "                                precond='RightSockOn',\n",
+       "                                effect='RightShoeOn'),\n",
+       "                        Action('RightSock',\n",
+       "                                precond='',\n",
+       "                                effect='RightSockOn'),\n",
+       "                        Action('LeftShoe',\n",
+       "                                precond='LeftSockOn',\n",
+       "                                effect='LeftShoeOn'),\n",
+       "                        Action('LeftSock',\n",
+       "                                precond='',\n",
+       "                                effect='LeftSockOn')])\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(socks_and_shoes)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**LeftSockOn:** Indicates that we have already put on the left sock.\n", + "\n", + "**RightSockOn:** Indicates that we have already put on the right sock.\n", + "\n", + "**LeftShoeOn:** Indicates that we have already put on the left shoe.\n", + "\n", + "**RightShoeOn:** Indicates that we have already put on the right shoe.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "socksShoes = socks_and_shoes()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first check whether the goal state is reached or not." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "socksShoes.goal_test()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the goal state isn't reached, we will define a sequence of actions that might help us achieve the goal.\n", + "These actions will then be acted upon the `socksShoes` PDDL to check if the goal state is reached." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "solution = [expr('RightSock'),\n", + " expr('RightShoe'),\n", + " expr('LeftSock'),\n", + " expr('LeftShoe')]" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "for action in solution:\n", + " socksShoes.act(action)\n", + " \n", + "socksShoes.goal_test()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have reached our goal." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cake Problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This problem requires us to reach the state of having a cake and having eaten a cake simlutaneously, given a single cake.\n", + "Let's first take a look at the definition of the `have_cake_and_eat_cake_too` problem in the module." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def have_cake_and_eat_cake_too():\n",
+       "    """\n",
+       "    [Figure 10.7] CAKE-PROBLEM\n",
+       "\n",
+       "    A problem where we begin with a cake and want to \n",
+       "    reach the state of having a cake and having eaten a cake.\n",
+       "    The possible actions include baking a cake and eating a cake.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> cp = have_cake_and_eat_cake_too()\n",
+       "    >>> cp.goal_test()\n",
+       "    False\n",
+       "    >>> cp.act(expr('Eat(Cake)'))\n",
+       "    >>> cp.goal_test()\n",
+       "    False\n",
+       "    >>> cp.act(expr('Bake(Cake)'))\n",
+       "    >>> cp.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
+       "\n",
+       "    return PDDL(init='Have(Cake)',\n",
+       "                goals='Have(Cake) & Eaten(Cake)',\n",
+       "                actions=[Action('Eat(Cake)',\n",
+       "                                precond='Have(Cake)',\n",
+       "                                effect='Eaten(Cake) & ~Have(Cake)'),\n",
+       "                         Action('Bake(Cake)',\n",
+       "                                precond='~Have(Cake)',\n",
+       "                                effect='Have(Cake)')])\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(have_cake_and_eat_cake_too)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since this problem doesn't involve variables, states can be considered similar to symbols in propositional logic.\n", + "\n", + "**Have(Cake):** Declares that we have a **'Cake'**.\n", + "\n", + "**~Have(Cake):** Declares that we _don't_ have a **'Cake'**." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "cakeProblem = have_cake_and_eat_cake_too()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First let us check whether the goal state 'Have(Cake)' and 'Eaten(Cake)' are reached or not." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n" + ] + } + ], + "source": [ + "print(cakeProblem.goal_test())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us look at the possible actions.\n", + "\n", + "**Bake(x):** To bake **' x '**.\n", + "\n", + "**Eat(x):** To eat **' x '**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now define a valid solution that can help us reach the goal.\n", + "The sequence of actions will then be acted upon the `cakeProblem` PDDL." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "solution = [expr(\"Eat(Cake)\"),\n", + " expr(\"Bake(Cake)\")]\n", + "\n", + "for action in solution:\n", + " cakeProblem.act(action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have made actions to bake the cake and eat the cake. Let us check if we have reached the goal." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], + "source": [ + "print(cakeProblem.goal_test())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It has now successfully achieved its goal i.e, to have and eat the cake." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One might wonder if the order of the actions matters for this problem.\n", + "Let's see for ourselves." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "ename": "Exception", + "evalue": "Action 'Bake(Cake)' pre-conditions not satisfied", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0maction\u001b[0m \u001b[1;32min\u001b[0m \u001b[0msolution\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 7\u001b[1;33m \u001b[0mcakeProblem\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mact\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32m~\\Documents\\Python\\Data Science\\Machine Learning\\Aima\\planning.py\u001b[0m in \u001b[0;36mact\u001b[1;34m(self, action)\u001b[0m\n\u001b[0;32m 58\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' not found\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction_name\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 59\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcheck_precond\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 60\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"Action '{}' pre-conditions not satisfied\"\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0maction\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 61\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mlist_action\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minit\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0margs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mclauses\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 62\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mException\u001b[0m: Action 'Bake(Cake)' pre-conditions not satisfied" + ] + } + ], + "source": [ + "cakeProblem = have_cake_and_eat_cake_too()\n", + "\n", + "solution = [expr('Bake(Cake)'),\n", + " expr('Eat(Cake)')]\n", + "\n", + "for action in solution:\n", + " cakeProblem.act(action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It raises an exception.\n", + "Indeed, according to the problem, we cannot bake a cake if we already have one.\n", + "In planning terms, '~Have(Cake)' is a precondition to the action 'Bake(Cake)'.\n", + "Hence, this solution is invalid." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SOLVING PLANNING PROBLEMS\n", + "----\n", + "### GRAPHPLAN\n", + "
\n", + "The GraphPlan algorithm is a popular method of solving classical planning problems.\n", + "Before we get into the details of the algorithm, let's look at a special data structure called **planning graph**, used to give better heuristic estimates and plays a key role in the GraphPlan algorithm." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Planning Graph\n", + "A planning graph is a directed graph organized into levels. \n", + "Each level contains information about the current state of the knowledge base and the possible state-action links to and from that level.\n", + "The first level contains the initial state with nodes representing each fluent that holds in that level.\n", + "This level has state-action links linking each state to valid actions in that state.\n", + "Each action is linked to all its preconditions and its effect states.\n", + "Based on these effects, the next level is constructed.\n", + "The next level contains similarly structured information about the next state.\n", + "In this way, the graph is expanded using state-action links till we reach a state where all the required goals hold true simultaneously.\n", + "We can say that we have reached our goal if none of the goal states in the current level are mutually exclusive.\n", "This will be explained in detail later.\n", "
\n", "Planning graphs only work for propositional planning problems, hence we need to eliminate all variables by generating all possible substitutions.\n", @@ -2214,7 +2746,7 @@ "
\n", "
\n", "The gray lines indicate mutual exclusivity.\n", - "This means that the actions connected by a gray line cannot be taken together.\n", + "This means that the actions connected bya gray line cannot be taken together.\n", "Mutual exclusivity (mutex) occurs in the following cases:\n", "1. **Inconsistent effects**: One action negates the effect of the other. For example, _Eat(Cake)_ and the persistence of _Have(Cake)_ have inconsistent effects because they disagree on the effect _Have(Cake)_\n", "2. **Interference**: One of the effects of an action is the negation of a precondition of the other. For example, _Eat(Cake)_ interferes with the persistence of _Have(Cake)_ by negating its precondition.\n", @@ -2231,7 +2763,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 50, "metadata": {}, "outputs": [ { @@ -2497,7 +3029,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 51, "metadata": {}, "outputs": [ { @@ -2656,7 +3188,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 52, "metadata": {}, "outputs": [ { @@ -2856,53 +3388,380 @@ } ], "source": [ - "psource(GraphPlan)" + "psource(GraphPlan)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Given a planning problem defined as a PDDL, `GraphPlan` creates a planning graph stored in `graph` and expands it till it reaches a state where all its required goals are present simultaneously without mutual exclusivity.\n", + "
\n", + "Once a goal is found, `extract_solution` is called.\n", + "This method recursively finds the path to a solution given a planning graph.\n", + "In the case where `extract_solution` fails to find a solution for a set of goals as a given level, we record the `(level, goals)` pair as a **no-good**.\n", + "Whenever `extract_solution` is called again with the same level and goals, we can find the recorded no-good and immediately return failure rather than searching again. \n", + "No-goods are also used in the termination test.\n", + "
\n", + "The `check_leveloff` method checks if the planning graph for the problem has **levelled-off**, ie, it has the same states, actions and mutex pairs as the previous level.\n", + "If the graph has already levelled off and we haven't found a solution, there is no point expanding the graph, as it won't lead to anything new.\n", + "In such a case, we can declare that the planning problem is unsolvable with the given constraints.\n", + "
\n", + "
\n", + "To summarize, the `GraphPlan` algorithm calls `expand_graph` and tests whether it has reached the goal and if the goals are non-mutex.\n", + "
\n", + "If so, `extract_solution` is invoked which recursively reconstructs the solution from the planning graph.\n", + "
\n", + "If not, then we check if our graph has levelled off and continue if it hasn't." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's solve a few planning problems that we had defined earlier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Air cargo problem\n", + "In accordance with the summary above, we have defined a helper function to carry out `GraphPlan` on the `air_cargo` problem.\n", + "The function is pretty straightforward.\n", + "Let's have a look." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def air_cargo_graphplan():\n",
+       "    """Solves the air cargo problem using GraphPlan"""\n",
+       "    return GraphPlan(air_cargo()).execute()\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(air_cargo_graphplan)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Given a planning problem defined as a PDDL, `GraphPlan` creates a planning graph stored in `graph` and expands it till it reaches a state where all its required goals are present simultaneously without mutual exclusivity.\n", - "
\n", - "Once a goal is found, `extract_solution` is called.\n", - "This method recursively finds the path to a solution given a planning graph.\n", - "In the case where `extract_solution` fails to find a solution for a set of goals as a given level, we record the `(level, goals)` pair as a **no-good**.\n", - "Whenever `extract_solution` is called again with the same level and goals, we can find the recorded no-good and immediately return failure rather than searching again. \n", - "No-goods are also used in the termination test.\n", - "
\n", - "The `check_leveloff` method checks if the planning graph for the problem has **levelled-off**, ie, it has the same states, actions and mutex pairs as the previous level.\n", - "If the graph has already levelled off and we haven't found a solution, there is no point expanding the graph, as it won't lead to anything new.\n", - "In such a case, we can declare that the planning problem is unsolvable with the given constraints.\n", - "
\n", - "
\n", - "To summarize, the `GraphPlan` algorithm calls `expand_graph` and tests whether it has reached the goal and if the goals are non-mutex.\n", + "Let's instantiate the problem and find a solution using this helper function." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " PCargo(C1),\n", + " PAirport(JFK),\n", + " PPlane(P2),\n", + " PAirport(SFO),\n", + " PPlane(P1),\n", + " PCargo(C2)],\n", + " [Unload(C2, P2, SFO), Unload(C1, P1, JFK)]]]" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "airCargoG = air_cargo_graphplan()\n", + "airCargoG" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each element in the solution is a valid action.\n", + "The solution is separated into lists for each level.\n", + "The actions prefixed with a 'P' are persistence actions and can be ignored.\n", + "They simply carry certain states forward.\n", + "We have another helper function `linearize` that presents the solution in a more readable format, much like a total-order planner, but it is _not_ a total-order planner." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Unload(C2, P2, SFO),\n", + " Unload(C1, P1, JFK)]" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "linearize(airCargoG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Indeed, this is a correct solution.\n", "
\n", - "If so, `extract_solution` is invoked which recursively reconstructs the solution from the planning graph.\n", + "There are similar helper functions for some other planning problems.\n", "
\n", - "If not, then we check if our graph has levelled off and continue if it hasn't." + "Lets' try solving the spare tire problem." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spareTireG = spare_tire_graphplan()\n", + "linearize(spareTireG)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's solve a few planning problems that we had defined earlier." + "Solution for the cake problem" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Eat(Cake), Bake(Cake)]" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cakeProblemG = have_cake_and_eat_cake_too_graphplan()\n", + "linearize(cakeProblemG)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Air cargo problem\n", - "In accordance with the summary above, we have defined a helper function to carry out `GraphPlan` on the `air_cargo` problem.\n", - "The function is pretty straightforward.\n", - "Let's have a look." + "Solution for the Sussman's Anomaly configuration of three blocks." ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sussmanAnomalyG = three_block_tower_graphplan()\n", + "linearize(sussmanAnomalyG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Solution of the socks and shoes problem" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[LeftSock, RightSock, LeftShoe, RightShoe]" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "socksShoesG = socks_and_shoes_graphplan()\n", + "linearize(socksShoesG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### TOTAL ORDER PLANNER\n", + "\n", + "In mathematical terminology, **total order**, **linear order** or **simple order** refers to a set *X* which is said to be totally ordered under ≤ if the following statements hold for all *a*, *b* and *c* in *X*:\n", + "
\n", + "If *a* ≤ *b* and *b* ≤ *a*, then *a* = *b* (antisymmetry).\n", + "
\n", + "If *a* ≤ *b* and *b* ≤ *c*, then *a* ≤ *c* (transitivity).\n", + "
\n", + "*a* ≤ *b* or *b* ≤ *a* (connex relation).\n", + "\n", + "
\n", + "In simpler terms, a total order plan is a linear ordering of actions to be taken to reach the goal state.\n", + "There may be several different total-order plans for a particular goal depending on the problem.\n", + "
\n", + "
\n", + "In the module, the `TotalOrderPlanner` class solves problems using this paradigm.\n", + "At its core, the `TotalOrderPlanner` uses a solved planning graph from `GraphPlan` and finds a valid total-order solution for it.\n", + "Let's have a look at the class." + ] + }, + { + "cell_type": "code", + "execution_count": 60, "metadata": {}, "outputs": [ { @@ -2994,9 +3853,54 @@ "\n", "

\n", "\n", - "
def air_cargo_graphplan():\n",
-       "    """Solves the air cargo problem using GraphPlan"""\n",
-       "    return GraphPlan(air_cargo()).execute()\n",
+       "
class TotalOrderPlanner:\n",
+       "\n",
+       "    def __init__(self, pddl):\n",
+       "        self.pddl = pddl\n",
+       "\n",
+       "    def filter(self, solution):\n",
+       "        """Filter out persistence actions from a solution"""\n",
+       "\n",
+       "        new_solution = []\n",
+       "        for section in solution[0]:\n",
+       "            new_section = []\n",
+       "            for operation in section:\n",
+       "                if not (operation.op[0] == 'P' and operation.op[1].isupper()):\n",
+       "                    new_section.append(operation)\n",
+       "            new_solution.append(new_section)\n",
+       "        return new_solution\n",
+       "\n",
+       "    def orderlevel(self, level, pddl):\n",
+       "        """Return valid linear order of actions for a given level"""\n",
+       "\n",
+       "        for permutation in itertools.permutations(level):\n",
+       "            temp = copy.deepcopy(pddl)\n",
+       "            count = 0\n",
+       "            for action in permutation:\n",
+       "                try:\n",
+       "                    temp.act(action)\n",
+       "                    count += 1\n",
+       "                except:\n",
+       "                    count = 0\n",
+       "                    temp = copy.deepcopy(pddl)\n",
+       "                    break\n",
+       "            if count == len(permutation):\n",
+       "                return list(permutation), temp\n",
+       "        return None\n",
+       "\n",
+       "    def execute(self):\n",
+       "        """Finds total-order solution for a planning graph"""\n",
+       "\n",
+       "        graphplan_solution = GraphPlan(self.pddl).execute()\n",
+       "        filtered_solution = self.filter(graphplan_solution)\n",
+       "        ordered_solution = []\n",
+       "        pddl = self.pddl\n",
+       "        for level in filtered_solution:\n",
+       "            level_solution, pddl = self.orderlevel(level, pddl)\n",
+       "            for element in level_solution:\n",
+       "                ordered_solution.append(element)\n",
+       "\n",
+       "        return ordered_solution\n",
        "
\n", "\n", "\n" @@ -3010,169 +3914,131 @@ } ], "source": [ - "psource(air_cargo_graphplan)" + "psource(TotalOrderPlanner)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's instantiate the problem and find a solution using this helper function." + "The `filter` method removes the persistence actions (if any) from the planning graph representation.\n", + "
\n", + "The `orderlevel` method finds a valid total-ordering of a specified level of the planning-graph, given the state of the graph after the previous level.\n", + "
\n", + "The `execute` method sequentially calls `orderlevel` for all the levels in the planning-graph and returns the final total-order solution.\n", + "
\n", + "
\n", + "Let's look at some examples." ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 61, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[[[PAirport(JFK),\n", - " Load(C2, P2, JFK),\n", - " Load(C1, P1, SFO)],\n", - " PPlane(P1),\n", - " PPlane(P2),\n", - " PAirport(SFO),\n", - " PCargo(C2),\n", - " Fly(P2, JFK, SFO),\n", - " PCargo(C1),\n", - " Fly(P1, SFO, JFK),\n", - " [Unload(C1, P1, JFK), Unload(C2, P2, SFO)]]]" + "[Load(C2, P2, JFK),\n", + " Fly(P2, JFK, SFO),\n", + " Load(C1, P1, SFO),\n", + " Fly(P1, SFO, JFK),\n", + " Unload(C2, P2, SFO),\n", + " Unload(C1, P1, JFK)]" ] }, - "execution_count": 44, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "air_cargo = air_cargo_graphplan()\n", - "air_cargo" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Each element in the solution is a valid action.\n", - "The solution is separated into lists for each level.\n", - "The actions prefixed with a 'P' are persistence actions and can be ignored.\n", - "They simply carry certain states forward.\n", - "We have another helper function `linearize` that presents the solution in a more readable format, much like a total-order planner, but it is _not_ a total-order planner." + "# total-order solution for air_cargo problem\n", + "TotalOrderPlanner(air_cargo()).execute()" ] }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 62, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Load(C2, P2, JFK),\n", - " Load(C1, P1, SFO),\n", - " Fly(P2, JFK, SFO),\n", - " Fly(P1, SFO, JFK),\n", - " Unload(C1, P1, JFK),\n", - " Unload(C2, P2, SFO)]" + "[Remove(Flat, Axle), Remove(Spare, Trunk), PutOn(Spare, Axle)]" ] }, - "execution_count": 45, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "linearize(air_cargo)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Indeed, this is a correct solution.\n", - "
\n", - "There are similar helper functions for some other planning problems.\n", - "
\n", - "Lets' try solving the spare tire problem." + "# total-order solution for spare_tire problem\n", + "TotalOrderPlanner(spare_tire()).execute()" ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 63, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]" + "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" ] }, - "execution_count": 46, + "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "spare_tire = spare_tire_graphplan()\n", - "linearize(spare_tire)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Solution for the cake problem" + "# total-order solution for three_block_tower problem\n", + "TotalOrderPlanner(three_block_tower()).execute()" ] }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 64, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Eat(Cake), Bake(Cake)]" + "[ToTable(A, B), FromTable(B, A), FromTable(C, B)]" ] }, - "execution_count": 47, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "cake_problem = have_cake_and_eat_cake_too_graphplan()\n", - "linearize(cake_problem)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Solution for the Sussman's Anomaly configuration of three blocks." + "# total-order solution for simple_blocks_world problem\n", + "TotalOrderPlanner(simple_blocks_world()).execute()" ] }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 65, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]" + "[LeftSock, RightSock, LeftShoe, RightShoe]" ] }, - "execution_count": 48, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sussman_anomaly = three_block_tower_graphplan()\n", - "linearize(sussman_anomaly)" + "# total-order solution for socks_and_shoes problem\n", + "TotalOrderPlanner(socks_and_shoes()).execute()" ] } ], From 62d0fcd27973d4965b2c33d4f7f6f0c76e3a4241 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 10:59:55 +0530 Subject: [PATCH 08/19] Added image --- images/pop.jpg | Bin 0 -> 109930 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/pop.jpg diff --git a/images/pop.jpg b/images/pop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52b3e3756c63f2102aa4d7345f09e2b320673989 GIT binary patch literal 109930 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%0D_!m~REL;%r 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 literal 0 HcmV?d00001 From 7a85bd2f855d3e0e919928be8c4d8cba031cf6fc Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 11:00:22 +0530 Subject: [PATCH 09/19] Added notebook section for PartialOrderPlanner --- planning.ipynb | 817 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 817 insertions(+) diff --git a/planning.ipynb b/planning.ipynb index 7dca111a2..d2c8430ac 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -4040,6 +4040,823 @@ "# total-order solution for socks_and_shoes problem\n", "TotalOrderPlanner(socks_and_shoes()).execute()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PARTIAL ORDER PLANNER\n", + "A partial-order planning algorithm is significantly different from a total-order planner.\n", + "The way a partial-order plan works enables it to take advantage of _problem decomposition_ and work on each subproblem separately.\n", + "It works on several subgoals independently, solves them with several subplans, and then combines the plan.\n", + "
\n", + "A partial-order planner also follows the **least commitment** strategy, where it delays making choices for as long as possible.\n", + "Variables are not bound unless it is absolutely necessary and new actions are chosen only if the existing actions cannot fulfil the required precondition.\n", + "
\n", + "Any planning algorithm that can place two actions into a plan without specifying which comes first is called a **partial-order planner**.\n", + "A partial-order planner searches through the space of plans rather than the space of states, which makes it perform better for certain problems.\n", + "
\n", + "
\n", + "Let's have a look at the `PartialOrderPlanner` class." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class PartialOrderPlanner:\n",
+       "\n",
+       "    def __init__(self, pddl):\n",
+       "        self.pddl = pddl\n",
+       "        self.initialize()\n",
+       "\n",
+       "    def initialize(self):\n",
+       "        """Initialize all variables"""\n",
+       "        self.causal_links = []\n",
+       "        self.start = Action('Start', [], self.pddl.init)\n",
+       "        self.finish = Action('Finish', self.pddl.goals, [])\n",
+       "        self.actions = set()\n",
+       "        self.actions.add(self.start)\n",
+       "        self.actions.add(self.finish)\n",
+       "        self.constraints = set()\n",
+       "        self.constraints.add((self.start, self.finish))\n",
+       "        self.agenda = set()\n",
+       "        for precond in self.finish.precond:\n",
+       "            self.agenda.add((precond, self.finish))\n",
+       "        self.expanded_actions = self.expand_actions()\n",
+       "\n",
+       "    def expand_actions(self, name=None):\n",
+       "        """Generate all possible actions with variable bindings for precondition selection heuristic"""\n",
+       "\n",
+       "        objects = set(arg for clause in self.pddl.init for arg in clause.args)\n",
+       "        expansions = []\n",
+       "        action_list = []\n",
+       "        if name is not None:\n",
+       "            for action in self.pddl.actions:\n",
+       "                if str(action.name) == name:\n",
+       "                    action_list.append(action)\n",
+       "        else:\n",
+       "            action_list = self.pddl.actions\n",
+       "\n",
+       "        for action in action_list:\n",
+       "            for permutation in itertools.permutations(objects, len(action.args)):\n",
+       "                bindings = unify(Expr(action.name, *action.args), Expr(action.name, *permutation))\n",
+       "                if bindings is not None:\n",
+       "                    new_args = []\n",
+       "                    for arg in action.args:\n",
+       "                        if arg in bindings:\n",
+       "                            new_args.append(bindings[arg])\n",
+       "                        else:\n",
+       "                            new_args.append(arg)\n",
+       "                    new_expr = Expr(str(action.name), *new_args)\n",
+       "                    new_preconds = []\n",
+       "                    for precond in action.precond:\n",
+       "                        new_precond_args = []\n",
+       "                        for arg in precond.args:\n",
+       "                            if arg in bindings:\n",
+       "                                new_precond_args.append(bindings[arg])\n",
+       "                            else:\n",
+       "                                new_precond_args.append(arg)\n",
+       "                        new_precond = Expr(str(precond.op), *new_precond_args)\n",
+       "                        new_preconds.append(new_precond)\n",
+       "                    new_effects = []\n",
+       "                    for effect in action.effect:\n",
+       "                        new_effect_args = []\n",
+       "                        for arg in effect.args:\n",
+       "                            if arg in bindings:\n",
+       "                                new_effect_args.append(bindings[arg])\n",
+       "                            else:\n",
+       "                                new_effect_args.append(arg)\n",
+       "                        new_effect = Expr(str(effect.op), *new_effect_args)\n",
+       "                        new_effects.append(new_effect)\n",
+       "                    expansions.append(Action(new_expr, new_preconds, new_effects))\n",
+       "\n",
+       "        return expansions\n",
+       "\n",
+       "    def find_open_precondition(self):\n",
+       "        """Find open precondition with the least number of possible actions"""\n",
+       "\n",
+       "        number_of_ways = dict()\n",
+       "        actions_for_precondition = dict()\n",
+       "        for element in self.agenda:\n",
+       "            open_precondition = element[0]\n",
+       "            possible_actions = list(self.actions) + self.expanded_actions\n",
+       "            for action in possible_actions:\n",
+       "                for effect in action.effect:\n",
+       "                    if effect == open_precondition:\n",
+       "                        if open_precondition in number_of_ways:\n",
+       "                            number_of_ways[open_precondition] += 1\n",
+       "                            actions_for_precondition[open_precondition].append(action)\n",
+       "                        else:\n",
+       "                            number_of_ways[open_precondition] = 1\n",
+       "                            actions_for_precondition[open_precondition] = [action]\n",
+       "\n",
+       "        number = sorted(number_of_ways, key=number_of_ways.__getitem__)\n",
+       "        \n",
+       "        for k, v in number_of_ways.items():\n",
+       "            if v == 0:\n",
+       "                return None, None, None\n",
+       "\n",
+       "        act1 = None\n",
+       "        for element in self.agenda:\n",
+       "            if element[0] == number[0]:\n",
+       "                act1 = element[1]\n",
+       "                break\n",
+       "\n",
+       "        if number[0] in self.expanded_actions:\n",
+       "            self.expanded_actions.remove(number[0])\n",
+       "\n",
+       "        return number[0], act1, actions_for_precondition[number[0]]\n",
+       "\n",
+       "    def find_action_for_precondition(self, oprec):\n",
+       "        """Find action for a given precondition"""\n",
+       "\n",
+       "        # either\n",
+       "        #   choose act0 E Actions such that act0 achieves G\n",
+       "        for action in self.actions:\n",
+       "            for effect in action.effect:\n",
+       "                if effect == oprec:\n",
+       "                    return action, 0\n",
+       "\n",
+       "        # or\n",
+       "        #   choose act0 E Actions such that act0 achieves G\n",
+       "        for action in self.pddl.actions:\n",
+       "            for effect in action.effect:\n",
+       "                if effect.op == oprec.op:\n",
+       "                    bindings = unify(effect, oprec)\n",
+       "                    if bindings is None:\n",
+       "                        break\n",
+       "                    return action, bindings\n",
+       "\n",
+       "    def generate_expr(self, clause, bindings):\n",
+       "        """Generate atomic expression from generic expression given variable bindings"""\n",
+       "\n",
+       "        new_args = []\n",
+       "        for arg in clause.args:\n",
+       "            if arg in bindings:\n",
+       "                new_args.append(bindings[arg])\n",
+       "            else:\n",
+       "                new_args.append(arg)\n",
+       "\n",
+       "        try:\n",
+       "            return Expr(str(clause.name), *new_args)\n",
+       "        except:\n",
+       "            return Expr(str(clause.op), *new_args)\n",
+       "        \n",
+       "    def generate_action_object(self, action, bindings):\n",
+       "        """Generate action object given a generic action andvariable bindings"""\n",
+       "\n",
+       "        # if bindings is 0, it means the action already exists in self.actions\n",
+       "        if bindings == 0:\n",
+       "            return action\n",
+       "\n",
+       "        # bindings cannot be None\n",
+       "        else:\n",
+       "            new_expr = self.generate_expr(action, bindings)\n",
+       "            new_preconds = []\n",
+       "            for precond in action.precond:\n",
+       "                new_precond = self.generate_expr(precond, bindings)\n",
+       "                new_preconds.append(new_precond)\n",
+       "            new_effects = []\n",
+       "            for effect in action.effect:\n",
+       "                new_effect = self.generate_expr(effect, bindings)\n",
+       "                new_effects.append(new_effect)\n",
+       "            return Action(new_expr, new_preconds, new_effects)\n",
+       "\n",
+       "    def cyclic(self, graph):\n",
+       "        """Check cyclicity of a directed graph"""\n",
+       "\n",
+       "        new_graph = dict()\n",
+       "        for element in graph:\n",
+       "            if element[0] in new_graph:\n",
+       "                new_graph[element[0]].append(element[1])\n",
+       "            else:\n",
+       "                new_graph[element[0]] = [element[1]]\n",
+       "\n",
+       "        path = set()\n",
+       "\n",
+       "        def visit(vertex):\n",
+       "            path.add(vertex)\n",
+       "            for neighbor in new_graph.get(vertex, ()):\n",
+       "                if neighbor in path or visit(neighbor):\n",
+       "                    return True\n",
+       "            path.remove(vertex)\n",
+       "            return False\n",
+       "\n",
+       "        value = any(visit(v) for v in new_graph)\n",
+       "        return value\n",
+       "\n",
+       "    def add_const(self, constraint, constraints):\n",
+       "        """Add the constraint to constraints if the resulting graph is acyclic"""\n",
+       "\n",
+       "        if constraint[0] == self.finish or constraint[1] == self.start:\n",
+       "            return constraints\n",
+       "\n",
+       "        new_constraints = set(constraints)\n",
+       "        new_constraints.add(constraint)\n",
+       "\n",
+       "        if self.cyclic(new_constraints):\n",
+       "            return constraints\n",
+       "        return new_constraints\n",
+       "\n",
+       "    def is_a_threat(self, precondition, effect):\n",
+       "        """Check if effect is a threat to precondition"""\n",
+       "\n",
+       "        if (str(effect.op) == 'Not' + str(precondition.op)) or ('Not' + str(effect.op) == str(precondition.op)):\n",
+       "            if effect.args == precondition.args:\n",
+       "                return True\n",
+       "        return False\n",
+       "\n",
+       "    def protect(self, causal_link, action, constraints):\n",
+       "        """Check and resolve threats by promotion or demotion"""\n",
+       "\n",
+       "        threat = False\n",
+       "        for effect in action.effect:\n",
+       "            if self.is_a_threat(causal_link[1], effect):\n",
+       "                threat = True\n",
+       "                break\n",
+       "\n",
+       "        if action != causal_link[0] and action != causal_link[2] and threat:\n",
+       "            # try promotion\n",
+       "            new_constraints = set(constraints)\n",
+       "            new_constraints.add((action, causal_link[0]))\n",
+       "            if not self.cyclic(new_constraints):\n",
+       "                constraints = self.add_const((action, causal_link[0]), constraints)\n",
+       "            else:\n",
+       "                # try demotion\n",
+       "                new_constraints = set(constraints)\n",
+       "                new_constraints.add((causal_link[2], action))\n",
+       "                if not self.cyclic(new_constraints):\n",
+       "                    constraints = self.add_const((causal_link[2], action), constraints)\n",
+       "                else:\n",
+       "                    # both promotion and demotion fail\n",
+       "                    print('Unable to resolve a threat caused by', action, 'onto', causal_link)\n",
+       "                    return\n",
+       "        return constraints\n",
+       "\n",
+       "    def convert(self, constraints):\n",
+       "        """Convert constraints into a dict of Action to set orderings"""\n",
+       "\n",
+       "        graph = dict()\n",
+       "        for constraint in constraints:\n",
+       "            if constraint[0] in graph:\n",
+       "                graph[constraint[0]].add(constraint[1])\n",
+       "            else:\n",
+       "                graph[constraint[0]] = set()\n",
+       "                graph[constraint[0]].add(constraint[1])\n",
+       "        return graph\n",
+       "\n",
+       "    def toposort(self, graph):\n",
+       "        """Generate topological ordering of constraints"""\n",
+       "\n",
+       "        if len(graph) == 0:\n",
+       "            return\n",
+       "\n",
+       "        graph = graph.copy()\n",
+       "\n",
+       "        for k, v in graph.items():\n",
+       "            v.discard(k)\n",
+       "\n",
+       "        extra_elements_in_dependencies = _reduce(set.union, graph.values()) - set(graph.keys())\n",
+       "\n",
+       "        graph.update({element:set() for element in extra_elements_in_dependencies})\n",
+       "        while True:\n",
+       "            ordered = set(element for element, dependency in graph.items() if len(dependency) == 0)\n",
+       "            if not ordered:\n",
+       "                break\n",
+       "            yield ordered\n",
+       "            graph = {element: (dependency - ordered) for element, dependency in graph.items() if element not in ordered}\n",
+       "        if len(graph) != 0:\n",
+       "            raise ValueError('The graph is not acyclic and cannot be linearly ordered')\n",
+       "\n",
+       "    def display_plan(self):\n",
+       "        """Display causal links, constraints and the plan"""\n",
+       "\n",
+       "        print('Causal Links')\n",
+       "        for causal_link in self.causal_links:\n",
+       "            print(causal_link)\n",
+       "\n",
+       "        print('\\nConstraints')\n",
+       "        for constraint in self.constraints:\n",
+       "            print(constraint[0], '<', constraint[1])\n",
+       "\n",
+       "        print('\\nPartial Order Plan')\n",
+       "        print(list(reversed(list(self.toposort(self.convert(self.constraints))))))\n",
+       "\n",
+       "    def execute(self, display=True):\n",
+       "        """Execute the algorithm"""\n",
+       "\n",
+       "        step = 1\n",
+       "        self.tries = 1\n",
+       "        while len(self.agenda) > 0:\n",
+       "            step += 1\n",
+       "            # select <G, act1> from Agenda\n",
+       "            try:\n",
+       "                G, act1, possible_actions = self.find_open_precondition()\n",
+       "            except IndexError:\n",
+       "                print('Probably Wrong')\n",
+       "                break\n",
+       "\n",
+       "            act0 = possible_actions[0]\n",
+       "            # remove <G, act1> from Agenda\n",
+       "            self.agenda.remove((G, act1))\n",
+       "\n",
+       "            # For actions with variable number of arguments, use least commitment principle\n",
+       "            # act0_temp, bindings = self.find_action_for_precondition(G)\n",
+       "            # act0 = self.generate_action_object(act0_temp, bindings)\n",
+       "\n",
+       "            # Actions = Actions U {act0}\n",
+       "            self.actions.add(act0)\n",
+       "\n",
+       "            # Constraints = add_const(start < act0, Constraints)\n",
+       "            self.constraints = self.add_const((self.start, act0), self.constraints)\n",
+       "\n",
+       "            # for each CL E CausalLinks do\n",
+       "            #   Constraints = protect(CL, act0, Constraints)\n",
+       "            for causal_link in self.causal_links:\n",
+       "                self.constraints = self.protect(causal_link, act0, self.constraints)\n",
+       "\n",
+       "            # Agenda = Agenda U {<P, act0>: P is a precondition of act0}\n",
+       "            for precondition in act0.precond:\n",
+       "                self.agenda.add((precondition, act0))\n",
+       "\n",
+       "            # Constraints = add_const(act0 < act1, Constraints)\n",
+       "            self.constraints = self.add_const((act0, act1), self.constraints)\n",
+       "\n",
+       "            # CausalLinks U {<act0, G, act1>}\n",
+       "            if (act0, G, act1) not in self.causal_links:\n",
+       "                self.causal_links.append((act0, G, act1))\n",
+       "\n",
+       "            # for each A E Actions do\n",
+       "            #   Constraints = protect(<act0, G, act1>, A, Constraints)\n",
+       "            for action in self.actions:\n",
+       "                self.constraints = self.protect((act0, G, act1), action, self.constraints)\n",
+       "\n",
+       "            if step > 200:\n",
+       "                print('Couldn\\'t find a solution')\n",
+       "                return None, None\n",
+       "\n",
+       "        if display:\n",
+       "            self.display_plan()\n",
+       "        else:\n",
+       "            return self.constraints, self.causal_links                \n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(PartialOrderPlanner)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will first describe the data-structures and helper methods used, followed by the algorithm used to find a partial-order plan." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each plan has the following four components:\n", + "\n", + "1. **`actions`**: a set of actions that make up the steps of the plan.\n", + "`actions` is always a subset of `pddl.actions` the set of possible actions for the given planning problem. \n", + "The `start` and `finish` actions are dummy actions defined to bring uniformity to the problem. The `start` action has no preconditions and its effects constitute the initial state of the planning problem. \n", + "The `finish` action has no effects and its preconditions constitute the goal state of the planning problem.\n", + "The empty plan consists of just these two dummy actions.\n", + "2. **`constraints`**: a set of temporal constraints that define the order of performing the actions relative to each other.\n", + "`constraints` does not define a linear ordering, rather it usually represents a directed graph which is also acyclic if the plan is consistent.\n", + "Each ordering is of the form A < B, which reads as \"A before B\" and means that action A _must_ be executed sometime before action B, but not necessarily immediately before.\n", + "`constraints` stores these as a set of tuples `(Action(A), Action(B))` which is interpreted as given above.\n", + "A constraint cannot be added to `constraints` if it breaks the acyclicity of the existing graph.\n", + "3. **`causal_links`**: a set of causal-links. \n", + "A causal link between two actions _A_ and _B_ in the plan is written as _A_ --_p_--> _B_ and is read as \"A achieves p for B\".\n", + "This imples that _p_ is an effect of _A_ and a precondition of _B_.\n", + "It also asserts that _p_ must remain true from the time of action _A_ to the time of action _B_.\n", + "Any violation of this rule is called a threat and must be resolved immediately by adding suitable ordering constraints.\n", + "`causal_links` stores this information as tuples `(Action(A), precondition(p), Action(B))` which is interpreted as given above.\n", + "Causal-links can also be called **protection-intervals**, because the link _A_ --_p_--> _B_ protects _p_ from being negated over the interval from _A_ to _B_.\n", + "4. **`agenda`**: a set of open-preconditions.\n", + "A precondition is open if it is not achieved by some action in the plan.\n", + "Planners will work to reduce the set of open preconditions to the empty set, without introducing a contradiction.\n", + "`agenda` stored this information as tuples `(precondition(p), Action(A))` where p is a precondition of the action A.\n", + "\n", + "A **consistent plan** is a plan in which there are no cycles in the ordering constraints and no conflicts with the causal-links.\n", + "A consistent plan with no open preconditions is a **solution**.\n", + "
\n", + "
\n", + "Let's briefly glance over the helper functions before going into the actual algorithm.\n", + "
\n", + "**`expand_actions`**: generates all possible actions with variable bindings for use as a heuristic of selection of an open precondition.\n", + "
\n", + "**`find_open_precondition`**: finds a precondition from the agenda with the least number of actions that fulfil that precondition.\n", + "This heuristic helps form mandatory ordering constraints and causal-links to further simplify the problem and reduce the probability of encountering a threat.\n", + "
\n", + "**`find_action_for_precondition`**: finds an action that fulfils the given precondition along with the absolutely necessary variable bindings in accordance with the principle of _least commitment_.\n", + "In case of multiple possible actions, the action with the least number of effects is chosen to minimize the chances of encountering a threat.\n", + "
\n", + "**`cyclic`**: checks if a directed graph is cyclic.\n", + "
\n", + "**`add_const`**: adds `constraint` to `constraints` if the newly formed graph is acyclic and returns `constraints` otherwise.\n", + "
\n", + "**`is_a_threat`**: checks if the given `effect` negates the given `precondition`.\n", + "
\n", + "**`protect`**: checks if the given `action` poses a threat to the given `causal_link`.\n", + "If so, the threat is resolved by either promotion or demotion, whichever generates acyclic temporal constraints.\n", + "If neither promotion or demotion work, the chosen action is not the correct fit or the planning problem cannot be solved altogether.\n", + "
\n", + "**`convert`**: converts a graph from a list of edges to an `Action` : `set` mapping, for use in topological sorting.\n", + "
\n", + "**`toposort`**: a generator function that generates a topological ordering of a given graph as a list of sets.\n", + "Each set contains an action or several actions.\n", + "If a set has more that one action in it, it means that permutations between those actions also produce a valid plan.\n", + "
\n", + "**`display_plan`**: displays the `causal_links`, `constraints` and the partial order plan generated from `toposort`.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The **`execute`** method executes the algorithm, which is summarized below:\n", + "
\n", + "1. An open precondition is selected (a sub-goal that we want to achieve).\n", + "2. An action that fulfils the open precondition is chosen.\n", + "3. Temporal constraints are updated.\n", + "4. Existing causal links are protected. Protection is a method that checks if the causal links conflict\n", + " and if they do, temporal constraints are added to fix the threats.\n", + "5. The set of open preconditions is updated.\n", + "6. Temporal constraints of the selected action and the next action are established.\n", + "7. A new causal link is added between the selected action and the owner of the open precondition.\n", + "8. The set of new causal links is checked for threats and if found, the threat is removed by either promotion or demotion.\n", + " If promotion or demotion is unable to solve the problem, the planning problem cannot be solved with the current sequence of actions\n", + " or it may not be solvable at all.\n", + "9. These steps are repeated until the set of open preconditions is empty." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A partial-order plan can be used to generate different valid total-order plans.\n", + "This step is called **linearization** of the partial-order plan.\n", + "All possible linearizations of a partial-order plan for `socks_and_shoes` looks like this.\n", + "
\n", + "![title](images/pop.jpg)\n", + "
\n", + "Linearization can be carried out in many ways, but the most efficient way is to represent the set of temporal constraints as a directed graph.\n", + "We can easily realize that the graph should also be acyclic as cycles in constraints means that the constraints are inconsistent.\n", + "This acyclicity is enforced by the `add_const` method, which adds a new constraint only if the acyclicity of the existing graph is not violated.\n", + "The `protect` method also checks for acyclicity of the newly-added temporal constraints to make a decision between promotion and demotion in case of a threat.\n", + "This property of a graph created from the temporal constraints of a valid partial-order plan allows us to use topological sort to order the constraints linearly.\n", + "A topological sort may produce several different valid solutions for a given directed acyclic graph." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we know how `PartialOrderPlanner` works, let's solve a few problems using it." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Causal Links\n", + "(Action(PutOn(Spare, Axle)), At(Spare, Axle), Action(Finish))\n", + "(Action(Start), Tire(Spare), Action(PutOn(Spare, Axle)))\n", + "(Action(Remove(Flat, Axle)), NotAt(Flat, Axle), Action(PutOn(Spare, Axle)))\n", + "(Action(Start), At(Flat, Axle), Action(Remove(Flat, Axle)))\n", + "(Action(Remove(Spare, Trunk)), At(Spare, Ground), Action(PutOn(Spare, Axle)))\n", + "(Action(Start), At(Spare, Trunk), Action(Remove(Spare, Trunk)))\n", + "(Action(Remove(Flat, Axle)), At(Flat, Ground), Action(Finish))\n", + "\n", + "Constraints\n", + "Action(Start) < Action(Finish)\n", + "Action(Start) < Action(Remove(Spare, Trunk))\n", + "Action(Remove(Flat, Axle)) < Action(PutOn(Spare, Axle))\n", + "Action(Remove(Flat, Axle)) < Action(Finish)\n", + "Action(Remove(Spare, Trunk)) < Action(PutOn(Spare, Axle))\n", + "Action(Start) < Action(PutOn(Spare, Axle))\n", + "Action(Start) < Action(Remove(Flat, Axle))\n", + "Action(PutOn(Spare, Axle)) < Action(Finish)\n", + "\n", + "Partial Order Plan\n", + "[{Action(Start)}, {Action(Remove(Flat, Axle)), Action(Remove(Spare, Trunk))}, {Action(PutOn(Spare, Axle))}, {Action(Finish)}]\n" + ] + } + ], + "source": [ + "st = spare_tire()\n", + "pop = PartialOrderPlanner(st)\n", + "pop.execute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We observe that in the given partial order plan, Remove(Flat, Axle) and Remove(Spare, Trunk) are in the same set.\n", + "This means that the order of performing these actions does not affect the final outcome.\n", + "That aside, we also see that the PutOn(Spare, Axle) action has to be performed after both the Remove actions are complete, which seems logically consistent." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Causal Links\n", + "(Action(FromTable(B, A)), On(B, A), Action(Finish))\n", + "(Action(FromTable(C, B)), On(C, B), Action(Finish))\n", + "(Action(Start), Clear(C), Action(FromTable(C, B)))\n", + "(Action(Start), Clear(A), Action(FromTable(B, A)))\n", + "(Action(Start), OnTable(C), Action(FromTable(C, B)))\n", + "(Action(Start), OnTable(B), Action(FromTable(B, A)))\n", + "(Action(ToTable(A, B)), Clear(B), Action(FromTable(C, B)))\n", + "(Action(Start), On(A, B), Action(ToTable(A, B)))\n", + "(Action(ToTable(A, B)), Clear(B), Action(FromTable(B, A)))\n", + "(Action(Start), Clear(A), Action(ToTable(A, B)))\n", + "\n", + "Constraints\n", + "Action(Start) < Action(FromTable(B, A))\n", + "Action(Start) < Action(FromTable(C, B))\n", + "Action(Start) < Action(ToTable(A, B))\n", + "Action(ToTable(A, B)) < Action(FromTable(C, B))\n", + "Action(Start) < Action(Finish)\n", + "Action(ToTable(A, B)) < Action(FromTable(B, A))\n", + "Action(FromTable(C, B)) < Action(Finish)\n", + "Action(FromTable(B, A)) < Action(Finish)\n", + "Action(FromTable(B, A)) < Action(FromTable(C, B))\n", + "\n", + "Partial Order Plan\n", + "[{Action(Start)}, {Action(ToTable(A, B))}, {Action(FromTable(B, A))}, {Action(FromTable(C, B))}, {Action(Finish)}]\n" + ] + } + ], + "source": [ + "sbw = simple_blocks_world()\n", + "pop = PartialOrderPlanner(sbw)\n", + "pop.execute()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "We see that this plan does not have flexibility in selecting actions, ie, actions should be performed in this order and this order only, to successfully reach the goal state." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Causal Links\n", + "(Action(RightShoe), RightShoeOn, Action(Finish))\n", + "(Action(LeftShoe), LeftShoeOn, Action(Finish))\n", + "(Action(LeftSock), LeftSockOn, Action(LeftShoe))\n", + "(Action(RightSock), RightSockOn, Action(RightShoe))\n", + "\n", + "Constraints\n", + "Action(Start) < Action(RightSock)\n", + "Action(Start) < Action(LeftSock)\n", + "Action(RightSock) < Action(RightShoe)\n", + "Action(RightShoe) < Action(Finish)\n", + "Action(Start) < Action(LeftShoe)\n", + "Action(LeftSock) < Action(LeftShoe)\n", + "Action(Start) < Action(RightShoe)\n", + "Action(Start) < Action(Finish)\n", + "Action(LeftShoe) < Action(Finish)\n", + "\n", + "Partial Order Plan\n", + "[{Action(Start)}, {Action(LeftSock), Action(RightSock)}, {Action(RightShoe), Action(LeftShoe)}, {Action(Finish)}]\n" + ] + } + ], + "source": [ + "ss = socks_and_shoes()\n", + "pop = PartialOrderPlanner(ss)\n", + "pop.execute()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "This plan again doesn't have constraints in selecting socks or shoes.\n", + "As long as both socks are worn before both shoes, we are fine.\n", + "Notice however, there is one valid solution,\n", + "
\n", + "LeftSock -> LeftShoe -> RightSock -> RightShoe\n", + "
\n", + "that the algorithm could not find as it cannot be represented as a general partially-ordered plan but is a specific total-order solution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Runtime differences\n", + "Let's briefly take a look at the running time of all the three algorithms on the `socks_and_shoes` problem." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "ss = socks_and_shoes()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "333 µs ± 8.86 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "GraphPlan(ss).execute()" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.29 ms ± 43.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "TotalOrderPlanner(ss).execute()" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "425 µs ± 17 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "PartialOrderPlanner(ss).execute(display=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We observe that `GraphPlan` is about 4 times faster than `TotalOrderPlanner` because `TotalOrderPlanner` essentially runs a `GraphPlan` subroutine under the hood and then carries out some transformations on the solved planning-graph.\n", + "
\n", + "We also find that `GraphPlan` is slightly faster than `PartialOrderPlanner`, but this is mainly due to the `expand_actions` method in `PartialOrderPlanner` that slows it down as it generates all possible permutations of actions and variable bindings.\n", + "
\n", + "Without heuristic functions, `PartialOrderPlanner` will be atleast as fast as `GraphPlan`, if not faster, but will have a higher tendency to encounter threats and conflicts which might take additional time to resolve.\n", + "
\n", + "Different planning algorithms work differently for different problems." + ] } ], "metadata": { From 85fda8a5d8d8b1e4aba105716d2dc9d897623755 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Thu, 24 May 2018 23:22:03 +0530 Subject: [PATCH 10/19] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bac287b6..301c16d36 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 10.3 | Three-Block-Tower | `three_block_tower` | [`planning.py`][planning] | Done | Included | | 10.7 | Cake-Problem | `have_cake_and_eat_cake_too` | [`planning.py`][planning] | Done | Included | | 10.9 | Graphplan | `GraphPlan` | [`planning.py`][planning] | Done | Included | -| 10.13 | Partial-Order-Planner | | | | | +| 10.13 | Partial-Order-Planner | `PartialOrderPlanner` | [`planning.py`][planning] | Done | Included | | 11.1 | Job-Shop-Problem-With-Resources | `job_shop_problem` | [`planning.py`][planning] | Done | | | 11.5 | Hierarchical-Search | `hierarchical_search` | [`planning.py`][planning] | | | | 11.8 | Angelic-Search | | | | | From 7b7e28aa2c2eb84ff8a560e91a7cdb60fdb549aa Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Fri, 25 May 2018 00:18:40 +0530 Subject: [PATCH 11/19] Refactor double tennis problem --- planning.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/planning.py b/planning.py index b44572c93..b485e74f4 100644 --- a/planning.py +++ b/planning.py @@ -152,6 +152,16 @@ def act(self, kb, args): return kb +def goal_test(goals, state): + """Generic goal testing helper function""" + + if isinstance(state, list): + kb = FolKB(state) + else: + kb = state + return all(kb.ask(q) is not False for q in goals) + + def air_cargo(): """ [Figure 10.1] AIR-CARGO-PROBLEM @@ -392,12 +402,31 @@ def socks_and_shoes(): def double_tennis_problem(): - """Doubles tennis problem""" + """ + [Figure 11.10] DOUBLE-TENNIS-PROBLEM + + A multiagent planning problem involving two partner tennis players + trying to return an approaching ball and repositioning around in the court. + + Example: + >>> from planning import * + >>> dtp = double_tennis_problem() + >>> goal_test(dtp.goals, dtp.init) + False + >>> dtp.act(expr('Go(A, RightBaseLine, LeftBaseLine)')) + >>> dtp.act(expr('Hit(A, Ball, RightBaseLine)')) + >>> goal_test(dtp.goals, dtp.init) + False + >>> dtp.act(expr('Go(A, LeftNet, RightBaseLine)')) + >>> goal_test(dtp.goals, dtp.init) + True + >>> + """ return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)', goals='Returned(Ball) & At(a, LeftNet) & At(a, RightNet)', actions=[Action('Hit(actor, Ball, loc)', - precond='Approaching(Ball,loc) & At(actor,loc)', + precond='Approaching(Ball, loc) & At(actor, loc)', effect='Returned(Ball)'), Action('Go(actor, to, loc)', precond='At(actor, loc)', From 40592c38c26c8b5d3d553e6a4ad6bb08dc97ecd1 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Fri, 25 May 2018 00:23:09 +0530 Subject: [PATCH 12/19] Refactored test for double_tennis_problem --- tests/test_planning.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_planning.py b/tests/test_planning.py index 4a2ce1fa6..3b29507f3 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -250,18 +250,18 @@ def test_partial_order_planner(): assert list(plan[3])[0].name == 'Finish' -# def test_double_tennis(): -# p = double_tennis_problem -# assert p.goal_test() is False +def test_double_tennis(): + p = double_tennis_problem() + assert not goal_test(p.goals, p.init) -# solution = [expr("Go(A, RightBaseLine, LeftBaseLine)"), -# expr("Hit(A, Ball, RightBaseLine)"), -# expr("Go(A, LeftNet, RightBaseLine)")] + solution = [expr("Go(A, RightBaseLine, LeftBaseLine)"), + expr("Hit(A, Ball, RightBaseLine)"), + expr("Go(A, LeftNet, RightBaseLine)")] -# for action in solution: -# p.act(action) + for action in solution: + p.act(action) -# assert p.goal_test() + assert goal_test(p.goals, p.init) def test_job_shop_problem(): From e09ebbd9b1a405dc372096f9c0499c25d352238c Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Fri, 25 May 2018 00:29:45 +0530 Subject: [PATCH 13/19] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 301c16d36..52a16b06b 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 11.1 | Job-Shop-Problem-With-Resources | `job_shop_problem` | [`planning.py`][planning] | Done | | | 11.5 | Hierarchical-Search | `hierarchical_search` | [`planning.py`][planning] | | | | 11.8 | Angelic-Search | | | | | -| 11.10 | Doubles-tennis | `double_tennis_problem` | [`planning.py`][planning] | | | +| 11.10 | Doubles-tennis | `double_tennis_problem` | [`planning.py`][planning] | Done | | | 13 | Discrete Probability Distribution | `ProbDist` | [`probability.py`][probability] | Done | Included | | 13.1 | DT-Agent | `DTAgent` | [`probability.py`][probability] | | | | 14.9 | Enumeration-Ask | `enumeration_ask` | [`probability.py`][probability] | Done | Included | From 9457732d2bf81c40eea35aa1ca866b5cc7abd48e Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Sat, 26 May 2018 02:36:48 +0530 Subject: [PATCH 14/19] Added notebook sections for job_shop_problem and double_tennis_problem --- planning.ipynb | 1029 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1027 insertions(+), 2 deletions(-) diff --git a/planning.ipynb b/planning.ipynb index d2c8430ac..092cf16cb 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -44,6 +44,7 @@ "source": [ "## CONTENTS\n", "\n", + "**Classical Planning**\n", "- PDDL\n", "- Action\n", "- Planning Problems\n", @@ -56,7 +57,18 @@ "- Solving Planning Problems\n", " * GraphPlan\n", " * TotalOrderPlanner\n", - " * PartiaOrderPlanner" + " * PartialOrderPlanner\n", + "
\n", + "\n", + "**Planning in the real world**\n", + "- Problem\n", + "- HLA\n", + "- Planning Problems\n", + " * Job shop problem\n", + " * Double tennis problem\n", + "- Solving Planning Problems\n", + " * Hierarchical Search\n", + " * Angelic Search" ] }, { @@ -1809,7 +1821,9 @@ { "cell_type": "code", "execution_count": 32, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "solution = [expr('ToTable(A, B)'),\n", @@ -4857,6 +4871,1017 @@ "
\n", "Different planning algorithms work differently for different problems." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PLANNING IN THE REAL WORLD\n", + "---\n", + "## PROBLEM\n", + "The `Problem` class is a wrapper for `PDDL` with some additional functionality and data-structures to handle real-world planning problems that involve time and resource constraints.\n", + "The `Problem` class includes everything that the `PDDL` class includes.\n", + "Additionally, it also includes the following attributes essential to define a real-world planning problem:\n", + "- a list of `jobs` to be done\n", + "- a dictionary of `resources`\n", + "\n", + "It also overloads the `act` method to call the `do_action` method of the `HLA` class, \n", + "and also includes a new method `refinements` that finds refinements or primitive actions for high level actions.\n", + "
\n", + "`hierarchical_search` and `angelic_search` are also built into the `Problem` class to solve such planning problems." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class Problem(PDDL):\n",
+       "    """\n",
+       "    Define real-world problems by aggregating resources as numerical quantities instead of\n",
+       "    named entities.\n",
+       "\n",
+       "    This class is identical to PDLL, except that it overloads the act function to handle\n",
+       "    resource and ordering conditions imposed by HLA as opposed to Action.\n",
+       "    """\n",
+       "    def __init__(self, init, goals, actions, jobs=None, resources=None):\n",
+       "        super().__init__(init, goals, actions)\n",
+       "        self.jobs = jobs\n",
+       "        self.resources = resources or {}\n",
+       "\n",
+       "    def act(self, action):\n",
+       "        """\n",
+       "        Performs the HLA given as argument.\n",
+       "\n",
+       "        Note that this is different from the superclass action - where the parameter was an\n",
+       "        Expression. For real world problems, an Expr object isn't enough to capture all the\n",
+       "        detail required for executing the action - resources, preconditions, etc need to be\n",
+       "        checked for too.\n",
+       "        """\n",
+       "        args = action.args\n",
+       "        list_action = first(a for a in self.actions if a.name == action.name)\n",
+       "        if list_action is None:\n",
+       "            raise Exception("Action '{}' not found".format(action.name))\n",
+       "        self.init = list_action.do_action(self.jobs, self.resources, self.init, args).clauses\n",
+       "\n",
+       "    def refinements(hla, state, library):  # TODO - refinements may be (multiple) HLA themselves ...\n",
+       "        """\n",
+       "        state is a Problem, containing the current state kb\n",
+       "        library is a dictionary containing details for every possible refinement. eg:\n",
+       "        {\n",
+       "        'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)', 'Taxi(Home, SFO)'],\n",
+       "        'steps': [['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], ['Taxi(Home, SFO)'], [], [], []],\n",
+       "        # empty refinements ie primitive action\n",
+       "        'precond': [['At(Home), Have(Car)'], ['At(Home)'], ['At(Home)', 'Have(Car)'], ['At(SFOLongTermParking)'], ['At(Home)']],\n",
+       "        'effect': [['At(SFO)'], ['At(SFO)'], ['At(SFOLongTermParking)'], ['At(SFO)'], ['At(SFO)'], ['~At(Home)'], ['~At(Home)'], ['~At(Home)'], ['~At(SFOLongTermParking)'], ['~At(Home)']]\n",
+       "        }\n",
+       "        """\n",
+       "        e = Expr(hla.name, hla.args)\n",
+       "        indices = [i for i, x in enumerate(library['HLA']) if expr(x).op == hla.name]\n",
+       "        for i in indices:\n",
+       "            # TODO multiple refinements\n",
+       "            precond = []\n",
+       "            for p in library['precond'][i]:\n",
+       "                if p[0] == '~':\n",
+       "                    precond.append(expr('Not' + p[1:]))\n",
+       "                else:\n",
+       "                    precond.append(expr(p))\n",
+       "            effect = []\n",
+       "            for e in library['effect'][i]:\n",
+       "                if e[0] == '~':\n",
+       "                    effect.append(expr('Not' + e[1:]))\n",
+       "                else:\n",
+       "                    effect.append(expr(e))\n",
+       "            action = HLA(library['steps'][i][0], precond, effect)\n",
+       "            if action.check_precond(state.init, action.args):\n",
+       "                yield action\n",
+       "\n",
+       "    def hierarchical_search(problem, hierarchy):\n",
+       "        """\n",
+       "        [Figure 11.5] 'Hierarchical Search, a Breadth First Search implementation of Hierarchical\n",
+       "        Forward Planning Search'\n",
+       "        The problem is a real-world problem defined by the problem class, and the hierarchy is\n",
+       "        a dictionary of HLA - refinements (see refinements generator for details)\n",
+       "        """\n",
+       "        act = Node(problem.actions[0])\n",
+       "        frontier = deque()\n",
+       "        frontier.append(act)\n",
+       "        while True:\n",
+       "            if not frontier:\n",
+       "                return None\n",
+       "            plan = frontier.popleft()\n",
+       "            print(plan.state.name)\n",
+       "            hla = plan.state  # first_or_null(plan)\n",
+       "            prefix = None\n",
+       "            if plan.parent:\n",
+       "                prefix = plan.parent.state.action  # prefix, suffix = subseq(plan.state, hla)\n",
+       "            outcome = Problem.result(problem, prefix)\n",
+       "            if hla is None:\n",
+       "                if outcome.goal_test():\n",
+       "                    return plan.path()\n",
+       "            else:\n",
+       "                print("else")\n",
+       "                for sequence in Problem.refinements(hla, outcome, hierarchy):\n",
+       "                    print("...")\n",
+       "                    frontier.append(Node(plan.state, plan.parent, sequence))\n",
+       "\n",
+       "    def result(problem, action):\n",
+       "        """The outcome of applying an action to the current problem"""\n",
+       "        if action is not None:\n",
+       "            problem.act(action)\n",
+       "            return problem\n",
+       "        else:\n",
+       "            return problem\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(Problem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HLA\n", + "To be able to model a real-world planning problem properly, it is essential to be able to represent a _high-level action (HLA)_ that can be hierarchically reduced to primitive actions." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class HLA(Action):\n",
+       "    """\n",
+       "    Define Actions for the real-world (that may be refined further), and satisfy resource\n",
+       "    constraints.\n",
+       "    """\n",
+       "    unique_group = 1\n",
+       "\n",
+       "    def __init__(self, action, precond=None, effect=None, duration=0,\n",
+       "                 consume=None, use=None):\n",
+       "        """\n",
+       "        As opposed to actions, to define HLA, we have added constraints.\n",
+       "        duration holds the amount of time required to execute the task\n",
+       "        consumes holds a dictionary representing the resources the task consumes\n",
+       "        uses holds a dictionary representing the resources the task uses\n",
+       "        """\n",
+       "        precond = precond or [None]\n",
+       "        effect = effect or [None]\n",
+       "        super().__init__(action, precond, effect)\n",
+       "        self.duration = duration\n",
+       "        self.consumes = consume or {}\n",
+       "        self.uses = use or {}\n",
+       "        self.completed = False\n",
+       "        # self.priority = -1 #  must be assigned in relation to other HLAs\n",
+       "        # self.job_group = -1 #  must be assigned in relation to other HLAs\n",
+       "\n",
+       "    def do_action(self, job_order, available_resources, kb, args):\n",
+       "        """\n",
+       "        An HLA based version of act - along with knowledge base updation, it handles\n",
+       "        resource checks, and ensures the actions are executed in the correct order.\n",
+       "        """\n",
+       "        # print(self.name)\n",
+       "        if not self.has_usable_resource(available_resources):\n",
+       "            raise Exception('Not enough usable resources to execute {}'.format(self.name))\n",
+       "        if not self.has_consumable_resource(available_resources):\n",
+       "            raise Exception('Not enough consumable resources to execute {}'.format(self.name))\n",
+       "        if not self.inorder(job_order):\n",
+       "            raise Exception("Can't execute {} - execute prerequisite actions first".\n",
+       "                            format(self.name))\n",
+       "        kb = super().act(kb, args)  # update knowledge base\n",
+       "        for resource in self.consumes:  # remove consumed resources\n",
+       "            available_resources[resource] -= self.consumes[resource]\n",
+       "        self.completed = True  # set the task status to complete\n",
+       "        return kb\n",
+       "\n",
+       "    def has_consumable_resource(self, available_resources):\n",
+       "        """\n",
+       "        Ensure there are enough consumable resources for this action to execute.\n",
+       "        """\n",
+       "        for resource in self.consumes:\n",
+       "            if available_resources.get(resource) is None:\n",
+       "                return False\n",
+       "            if available_resources[resource] < self.consumes[resource]:\n",
+       "                return False\n",
+       "        return True\n",
+       "\n",
+       "    def has_usable_resource(self, available_resources):\n",
+       "        """\n",
+       "        Ensure there are enough usable resources for this action to execute.\n",
+       "        """\n",
+       "        for resource in self.uses:\n",
+       "            if available_resources.get(resource) is None:\n",
+       "                return False\n",
+       "            if available_resources[resource] < self.uses[resource]:\n",
+       "                return False\n",
+       "        return True\n",
+       "\n",
+       "    def inorder(self, job_order):\n",
+       "        """\n",
+       "        Ensure that all the jobs that had to be executed before the current one have been\n",
+       "        successfully executed.\n",
+       "        """\n",
+       "        for jobs in job_order:\n",
+       "            if self in jobs:\n",
+       "                for job in jobs:\n",
+       "                    if job is self:\n",
+       "                        return True\n",
+       "                    if not job.completed:\n",
+       "                        return False\n",
+       "        return True\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(HLA)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In addition to preconditions and effects, an object of the `HLA` class also stores:\n", + "- the `duration` of the HLA\n", + "- the quantity of consumption of _consumable_ resources\n", + "- the quantity of _reusable_ resources used\n", + "- a bool `completed` denoting if the `HLA` has been completed\n", + "\n", + "The class also has some useful helper methods:\n", + "- `do_action`: checks if required consumable and reusable resources are available and if so, executes the action.\n", + "- `has_consumable_resource`: checks if there exists sufficient quantity of the required consumable resource.\n", + "- `has_usable_resource`: checks if reusable resources are available and not already engaged.\n", + "- `inorder`: ensures that all the jobs that had to be executed before the current one have been successfully executed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PLANNING PROBLEMS\n", + "---\n", + "## Job-shop Problem\n", + "This is a simple problem involving the assembly of two cars simultaneously.\n", + "The problem consists of two jobs, each of the form [`AddEngine`, `AddWheels`, `Inspect`] to be performed on two cars with different requirements and availability of resources.\n", + "
\n", + "Let's look at how the `job_shop_problem` has been defined on the module." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def job_shop_problem():\n",
+       "    """\n",
+       "    [Figure 11.1] JOB-SHOP-PROBLEM\n",
+       "\n",
+       "    A job-shop scheduling problem for assembling two cars,\n",
+       "    with resource and ordering constraints.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> p = job_shop_problem()\n",
+       "    >>> p.goal_test()\n",
+       "    False\n",
+       "    >>> p.act(p.jobs[1][0])\n",
+       "    >>> p.act(p.jobs[1][1])\n",
+       "    >>> p.act(p.jobs[1][2])\n",
+       "    >>> p.act(p.jobs[0][0])\n",
+       "    >>> p.act(p.jobs[0][1])\n",
+       "    >>> p.goal_test()\n",
+       "    False\n",
+       "    >>> p.act(p.jobs[0][2])\n",
+       "    >>> p.goal_test()\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
+       "    resources = {'EngineHoists': 1, 'WheelStations': 2, 'Inspectors': 2, 'LugNuts': 500}\n",
+       "\n",
+       "    add_engine1 = HLA('AddEngine1', precond='~Has(C1, E1)', effect='Has(C1, E1)', duration=30, use={'EngineHoists': 1})\n",
+       "    add_engine2 = HLA('AddEngine2', precond='~Has(C2, E2)', effect='Has(C2, E2)', duration=60, use={'EngineHoists': 1})\n",
+       "    add_wheels1 = HLA('AddWheels1', precond='~Has(C1, W1)', effect='Has(C1, W1)', duration=30, use={'WheelStations': 1}, consume={'LugNuts': 20})\n",
+       "    add_wheels2 = HLA('AddWheels2', precond='~Has(C2, W2)', effect='Has(C2, W2)', duration=15, use={'WheelStations': 1}, consume={'LugNuts': 20})\n",
+       "    inspect1 = HLA('Inspect1', precond='~Inspected(C1)', effect='Inspected(C1)', duration=10, use={'Inspectors': 1})\n",
+       "    inspect2 = HLA('Inspect2', precond='~Inspected(C2)', effect='Inspected(C2)', duration=10, use={'Inspectors': 1})\n",
+       "\n",
+       "    actions = [add_engine1, add_engine2, add_wheels1, add_wheels2, inspect1, inspect2]\n",
+       "\n",
+       "    job_group1 = [add_engine1, add_wheels1, inspect1]\n",
+       "    job_group2 = [add_engine2, add_wheels2, inspect2]\n",
+       "\n",
+       "    return Problem(init='Car(C1) & Car(C2) & Wheels(W1) & Wheels(W2) & Engine(E2) & Engine(E2) & ~Has(C1, E1) & ~Has(C2, E2) & ~Has(C1, W1) & ~Has(C2, W2) & ~Inspected(C1) & ~Inspected(C2)',\n",
+       "                   goals='Has(C1, W1) & Has(C1, E1) & Inspected(C1) & Has(C2, W2) & Has(C2, E2) & Inspected(C2)',\n",
+       "                   actions=actions,\n",
+       "                   jobs=[job_group1, job_group2],\n",
+       "                   resources=resources)\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(job_shop_problem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The states of this problem are:\n", + "
\n", + "
\n", + "**Has(x, y)**: Car **'x'** _has_ **'y'** where **'y'** can be an Engine or a Wheel.\n", + "\n", + "**~Has(x, y)**: Car **'x'** does _not have_ **'y'** where **'y'** can be an Engine or a Wheel.\n", + "\n", + "**Inspected(c)**: Car **'c'** has been _inspected_.\n", + "\n", + "**~Inspected(c)**: Car **'c'** has _not_ been inspected.\n", + "\n", + "In the initial state, `C1` and `C2` are cars and neither have an engine or wheels and haven't been inspected.\n", + "`E1` and `E2` are engines.\n", + "`W1` and `W2` are wheels.\n", + "
\n", + "Our goal is to have engines and wheels on both cars and to get them inspected. We will discuss how to achieve this.\n", + "
\n", + "Let's define an object of the `job_shop_problem`." + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "jobShopProblem = job_shop_problem()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before taking any actions, we will check if `jobShopProblem` has reached its goal." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n" + ] + } + ], + "source": [ + "print(jobShopProblem.goal_test())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now define a possible solution that can help us reach the goal. \n", + "The actions are then carried out on the `jobShopProblem` object." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following actions are available to us:\n", + "\n", + "**AddEngine1**: Adds an engine to the car C1. Takes 30 minutes to complete and uses an engine hoist.\n", + " \n", + "**AddEngine2**: Adds an engine to the car C2. Takes 60 minutes to complete and uses an engine hoist.\n", + "\n", + "**AddWheels1**: Adds wheels to car C1. Takes 30 minutes to complete. Uses a wheel station and consumes 20 lug nuts.\n", + "\n", + "**AddWheels2**: Adds wheels to car C2. Takes 15 minutes to complete. Uses a wheel station and consumes 20 lug nuts as well.\n", + "\n", + "**Inspect1**: Gets car C1 inspected. Requires 10 minutes of inspection by one inspector.\n", + "\n", + "**Inspect2**: Gets car C2 inspected. Requires 10 minutes of inspection by one inspector." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "solution = [jobShopProblem.jobs[1][0],\n", + " jobShopProblem.jobs[1][1],\n", + " jobShopProblem.jobs[1][2],\n", + " jobShopProblem.jobs[0][0],\n", + " jobShopProblem.jobs[0][1],\n", + " jobShopProblem.jobs[0][2]]\n", + "\n", + "for action in solution:\n", + " jobShopProblem.act(action)" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n" + ] + } + ], + "source": [ + "print(jobShopProblem.goal_test())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a valid solution and one of many correct ways to solve this problem." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Double tennis problem\n", + "This problem is a simple case of a multiactor planning problem, where two agents act at once and can simultaneously change the current state of the problem. \n", + "A correct plan is one that, if executed by the actors, achieves the goal.\n", + "In the true multiagent setting, of course, the agents may not agree to execute any particular plan, but atleast they will know what plans _would_ work if they _did_ agree to execute them.\n", + "
\n", + "In the double tennis problem, two actors A and B are playing together and can be in one of four locations: `LeftBaseLine`, `RightBaseLine`, `LeftNet` and `RightNet`.\n", + "The ball can be returned only if a player is in the right place.\n", + "Each action must include the actor as an argument.\n", + "
\n", + "Let's first look at the definition of the `double_tennis_problem` in the module." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def double_tennis_problem():\n",
+       "    """\n",
+       "    [Figure 11.10] DOUBLE-TENNIS-PROBLEM\n",
+       "\n",
+       "    A multiagent planning problem involving two partner tennis players\n",
+       "    trying to return an approaching ball and repositioning around in the court.\n",
+       "\n",
+       "    Example:\n",
+       "    >>> from planning import *\n",
+       "    >>> dtp = double_tennis_problem()\n",
+       "    >>> goal_test(dtp.goals, dtp.init)\n",
+       "    False\n",
+       "    >>> dtp.act(expr('Go(A, RightBaseLine, LeftBaseLine)'))\n",
+       "    >>> dtp.act(expr('Hit(A, Ball, RightBaseLine)'))\n",
+       "    >>> goal_test(dtp.goals, dtp.init)\n",
+       "    False\n",
+       "    >>> dtp.act(expr('Go(A, LeftNet, RightBaseLine)'))\n",
+       "    >>> goal_test(dtp.goals, dtp.init)\n",
+       "    True\n",
+       "    >>>\n",
+       "    """\n",
+       "\n",
+       "    return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',\n",
+       "                             goals='Returned(Ball) & At(x, LeftNet) & At(y, RightNet)',\n",
+       "                             actions=[Action('Hit(actor, Ball, loc)',\n",
+       "                                             precond='Approaching(Ball, loc) & At(actor, loc)',\n",
+       "                                             effect='Returned(Ball)'),\n",
+       "                                      Action('Go(actor, to, loc)', \n",
+       "                                             precond='At(actor, loc)',\n",
+       "                                             effect='At(actor, to) & ~At(actor, loc)')])\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(double_tennis_problem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The states of this problem are:\n", + "\n", + "**Approaching(Ball, loc)**: The `Ball` is approaching the location `loc`.\n", + "\n", + "**Returned(Ball)**: One of the actors successfully hit the approaching ball from the correct location which caused it to return to the other side.\n", + "\n", + "**At(actor, loc)**: `actor` is at location `loc`.\n", + "\n", + "**~At(actor, loc)**: `actor` is _not_ at location `loc`.\n", + "\n", + "Let's now define an object of `double_tennis_problem`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "doubleTennisProblem = double_tennis_problem()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before taking any actions, we will check if `doubleTennisProblem` has reached the goal." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n" + ] + } + ], + "source": [ + "print(goal_test(doubleTennisProblem.goals, doubleTennisProblem.init))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the goal hasn't been reached. \n", + "We now define a possible solution that can help us reach the goal of having the ball returned.\n", + "The actions will then be carried out on the `doubleTennisProblem` object." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The actions available to us are the following:\n", + "\n", + "**Hit(actor, ball, loc)**: returns an approaching ball if `actor` is present at the `loc` that the ball is approaching.\n", + "\n", + "**Go(actor, to, loc)**: moves an `actor` from location `loc` to location `to`.\n", + "\n", + "We notice something different in this problem though, \n", + "which is quite unlike any other problem we have seen so far. \n", + "The goal state of the problem contains a variable `a`.\n", + "This happens sometimes in multiagent planning problems \n", + "and it means that it doesn't matter _which_ actor is at the `LeftNet` or the `RightNet`, as long as there is atleast one actor at either `LeftNet` or `RightNet`." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "solution = [expr('Go(A, RightBaseLine, LeftBaseLine)'),\n", + " expr('Hit(A, Ball, RightBaseLine)'),\n", + " expr('Go(A, LeftNet, RightBaseLine)')]\n", + "\n", + "for action in solution:\n", + " doubleTennisProblem.act(action)" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "goal_test(doubleTennisProblem.goals, doubleTennisProblem.init)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It has now successfully reached its goal, ie, to return the approaching ball." + ] } ], "metadata": { From 6e2dfb374ce5bd80e16b8dec9301eae76f41036b Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Sat, 26 May 2018 02:38:28 +0530 Subject: [PATCH 15/19] Updated README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52a16b06b..d89a90bca 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,10 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 10.7 | Cake-Problem | `have_cake_and_eat_cake_too` | [`planning.py`][planning] | Done | Included | | 10.9 | Graphplan | `GraphPlan` | [`planning.py`][planning] | Done | Included | | 10.13 | Partial-Order-Planner | `PartialOrderPlanner` | [`planning.py`][planning] | Done | Included | -| 11.1 | Job-Shop-Problem-With-Resources | `job_shop_problem` | [`planning.py`][planning] | Done | | +| 11.1 | Job-Shop-Problem-With-Resources | `job_shop_problem` | [`planning.py`][planning] | Done | Included | | 11.5 | Hierarchical-Search | `hierarchical_search` | [`planning.py`][planning] | | | | 11.8 | Angelic-Search | | | | | -| 11.10 | Doubles-tennis | `double_tennis_problem` | [`planning.py`][planning] | Done | | +| 11.10 | Doubles-tennis | `double_tennis_problem` | [`planning.py`][planning] | Done | Included | | 13 | Discrete Probability Distribution | `ProbDist` | [`probability.py`][probability] | Done | Included | | 13.1 | DT-Agent | `DTAgent` | [`probability.py`][probability] | | | | 14.9 | Enumeration-Ask | `enumeration_ask` | [`probability.py`][probability] | Done | Included | From 450cae39d9930bd2990a2c3ce2c21e21872a14f6 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Sat, 26 May 2018 17:19:39 +0530 Subject: [PATCH 16/19] Fixed refinements example --- planning.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/planning.py b/planning.py index b485e74f4..c4381fad1 100644 --- a/planning.py +++ b/planning.py @@ -1258,11 +1258,35 @@ def refinements(hla, state, library): # TODO - refinements may be (multiple) HL state is a Problem, containing the current state kb library is a dictionary containing details for every possible refinement. eg: { - 'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)', 'Taxi(Home, SFO)'], - 'steps': [['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], ['Taxi(Home, SFO)'], [], [], []], - # empty refinements ie primitive action - 'precond': [['At(Home), Have(Car)'], ['At(Home)'], ['At(Home)', 'Have(Car)'], ['At(SFOLongTermParking)'], ['At(Home)']], - 'effect': [['At(SFO)'], ['At(SFO)'], ['At(SFOLongTermParking)'], ['At(SFO)'], ['At(SFO)'], ['~At(Home)'], ['~At(Home)'], ['~At(Home)'], ['~At(SFOLongTermParking)'], ['~At(Home)']] + 'HLA': [ + 'Go(Home, SFO)', + 'Go(Home, SFO)', + 'Drive(Home, SFOLongTermParking)', + 'Shuttle(SFOLongTermParking, SFO)', + 'Taxi(Home, SFO)' + ], + 'steps': [ + ['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], + ['Taxi(Home, SFO)'], + [], + [], + [] + ], + # empty refinements indicate a primitive action + 'precond': [ + ['At(Home)', 'Have(Car)'], + ['At(Home)'], + ['At(Home)', 'Have(Car)'], + ['At(SFOLongTermParking)'], + ['At(Home)'] + ], + 'effect': [ + ['At(SFO)', '~At(Home)'], + ['At(SFO)', '~At(Home)'], + ['At(SFOLongTermParking)', '~At(Home)'], + ['At(SFO)', '~At(SFOLongTermParking)'], + ['At(SFO)', '~At(Home)'] + ] } """ e = Expr(hla.name, hla.args) From cfea5e61686fbb0f361f8dc487b415bc4e052465 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Sat, 26 May 2018 22:28:07 +0530 Subject: [PATCH 17/19] Added go_to_sfo problem --- planning.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/planning.py b/planning.py index c4381fad1..722e06700 100644 --- a/planning.py +++ b/planning.py @@ -1390,3 +1390,48 @@ def job_shop_problem(): actions=actions, jobs=[job_group1, job_group2], resources=resources) + + +def go_to_sfo(): + """Go to SFO Problem""" + + go_home_sfo1 = HLA('Go(Home, SFO)', precond='At(Home) & Have(Car)', effect='At(SFO) & ~At(Home)') + go_home_sfo2 = HLA('Go(Home, SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)') + drive_home_sfoltp = HLA('Drive(Home, SFOLongTermParking)', precond='At(Home) & Have(Car)', effect='At(SFOLongTermParking) & ~At(Home)') + shuttle_sfoltp_sfo = HLA('Shuttle(SFOLongTermParking, SFO)', precond='At(SFOLongTermParking)', effect='At(SFO) & ~At(SFOLongTermParking)') + taxi_home_sfo = HLA('Taxi(Home, SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)') + + actions = [go_home_sfo1, go_home_sfo2, drive_home_sfoltp, shuttle_sfoltp_sfo, taxi_home_sfo] + + library = { + 'HLA': [ + 'Go(Home, SFO)', + 'Go(Home, SFO)', + 'Drive(Home, SFOLongTermParking)', + 'Shuttle(SFOLongTermParking, SFO)', + 'Taxi(Home, SFO)' + ], + 'steps': [ + ['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], + ['Taxi(Home, SFO)'], + [], + [], + [] + ], + 'precond': [ + ['At(Home)', 'Have(Car)'], + ['At(Home)'], + ['At(Home)', 'Have(Car)'], + ['At(SFOLongTermParking)'], + ['At(Home)'] + ], + 'effect': [ + ['At(SFO)', '~At(Home)'], + ['At(SFO)', '~At(Home)'], + ['At(SFOLongTermParking)', '~At(Home)'], + ['At(SFO)', '~At(SFOLongTermParking)'], + ['At(SFO)', '~At(Home)'] + ] + } + + return Problem(init='At(Home)', goals='At(SFO)', actions=actions), library From f0154483963ca6b2e2764e6432df1e2914242242 Mon Sep 17 00:00:00 2001 From: AngryCracker Date: Mon, 4 Jun 2018 01:02:41 +0530 Subject: [PATCH 18/19] Rename TotalOrderPlanner --- planning.ipynb | 24 ++++++++++++------------ planning.py | 2 +- tests/test_planning.py | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/planning.ipynb b/planning.ipynb index 092cf16cb..e270a7bf6 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -56,7 +56,7 @@ " * Cake problem\n", "- Solving Planning Problems\n", " * GraphPlan\n", - " * TotalOrderPlanner\n", + " * Linearize\n", " * PartialOrderPlanner\n", "
\n", "\n", @@ -3768,8 +3768,8 @@ "There may be several different total-order plans for a particular goal depending on the problem.\n", "
\n", "
\n", - "In the module, the `TotalOrderPlanner` class solves problems using this paradigm.\n", - "At its core, the `TotalOrderPlanner` uses a solved planning graph from `GraphPlan` and finds a valid total-order solution for it.\n", + "In the module, the `Linearize` class solves problems using this paradigm.\n", + "At its core, the `Linearize` uses a solved planning graph from `GraphPlan` and finds a valid total-order solution for it.\n", "Let's have a look at the class." ] }, @@ -3867,7 +3867,7 @@ "\n", "

\n", "\n", - "
class TotalOrderPlanner:\n",
+       "
class Linearize:\n",
        "\n",
        "    def __init__(self, pddl):\n",
        "        self.pddl = pddl\n",
@@ -3928,7 +3928,7 @@
     }
    ],
    "source": [
-    "psource(TotalOrderPlanner)"
+    "psource(Linearize)"
    ]
   },
   {
@@ -3968,7 +3968,7 @@
    ],
    "source": [
     "# total-order solution for air_cargo problem\n",
-    "TotalOrderPlanner(air_cargo()).execute()"
+    "Linearize(air_cargo()).execute()"
    ]
   },
   {
@@ -3989,7 +3989,7 @@
    ],
    "source": [
     "# total-order solution for spare_tire problem\n",
-    "TotalOrderPlanner(spare_tire()).execute()"
+    "Linearize(spare_tire()).execute()"
    ]
   },
   {
@@ -4010,7 +4010,7 @@
    ],
    "source": [
     "# total-order solution for three_block_tower problem\n",
-    "TotalOrderPlanner(three_block_tower()).execute()"
+    "Linearize(three_block_tower()).execute()"
    ]
   },
   {
@@ -4031,7 +4031,7 @@
    ],
    "source": [
     "# total-order solution for simple_blocks_world problem\n",
-    "TotalOrderPlanner(simple_blocks_world()).execute()"
+    "Linearize(simple_blocks_world()).execute()"
    ]
   },
   {
@@ -4052,7 +4052,7 @@
    ],
    "source": [
     "# total-order solution for socks_and_shoes problem\n",
-    "TotalOrderPlanner(socks_and_shoes()).execute()"
+    "Linearize(socks_and_shoes()).execute()"
    ]
   },
   {
@@ -4838,7 +4838,7 @@
    ],
    "source": [
     "%%timeit\n",
-    "TotalOrderPlanner(ss).execute()"
+    "Linearize(ss).execute()"
    ]
   },
   {
@@ -4863,7 +4863,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "We observe that `GraphPlan` is about 4 times faster than `TotalOrderPlanner` because `TotalOrderPlanner` essentially runs a `GraphPlan` subroutine under the hood and then carries out some transformations on the solved planning-graph.\n",
+    "We observe that `GraphPlan` is about 4 times faster than `Linearize` because `Linearize` essentially runs a `GraphPlan` subroutine under the hood and then carries out some transformations on the solved planning-graph.\n",
     "
\n", "We also find that `GraphPlan` is slightly faster than `PartialOrderPlanner`, but this is mainly due to the `expand_actions` method in `PartialOrderPlanner` that slows it down as it generates all possible permutations of actions and variable bindings.\n", "
\n", diff --git a/planning.py b/planning.py index 722e06700..77406cffb 100644 --- a/planning.py +++ b/planning.py @@ -693,7 +693,7 @@ def execute(self): return None -class TotalOrderPlanner: +class Linearize: def __init__(self, pddl): self.pddl = pddl diff --git a/tests/test_planning.py b/tests/test_planning.py index 3b29507f3..926a76671 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -162,11 +162,11 @@ def test_graphplan(): assert expr('Buy(Milk, SM)') in shopping_problem_solution -def test_total_order_planner(): +def test_linearize_class(): st = spare_tire() possible_solutions = [[expr('Remove(Spare, Trunk)'), expr('Remove(Flat, Axle)'), expr('PutOn(Spare, Axle)')], [expr('Remove(Flat, Axle)'), expr('Remove(Spare, Trunk)'), expr('PutOn(Spare, Axle)')]] - assert TotalOrderPlanner(st).execute() in possible_solutions + assert Linearize(st).execute() in possible_solutions ac = air_cargo() possible_solutions = [[expr('Load(C1, P1, SFO)'), expr('Load(C2, P2, JFK)'), expr('Fly(P1, SFO, JFK)'), expr('Fly(P2, JFK, SFO)'), expr('Unload(C1, P1, JFK)'), expr('Unload(C2, P2, SFO)')], @@ -182,7 +182,7 @@ def test_total_order_planner(): [expr('Load(C2, P2, JFK)'), expr('Fly(P2, JFK, SFO)'), expr('Load(C1, P1, SFO)'), expr('Fly(P1, SFO, JFK)'), expr('Unload(C1, P1, JFK)'), expr('Unload(C2, P2, SFO)')], [expr('Load(C2, P2, JFK)'), expr('Fly(P2, JFK, SFO)'), expr('Load(C1, P1, SFO)'), expr('Fly(P1, SFO, JFK)'), expr('Unload(C2, P2, SFO)'), expr('Unload(C1, P1, JFK)')] ] - assert TotalOrderPlanner(ac).execute() in possible_solutions + assert Linearize(ac).execute() in possible_solutions ss = socks_and_shoes() possible_solutions = [[expr('LeftSock'), expr('RightSock'), expr('LeftShoe'), expr('RightShoe')], @@ -192,7 +192,7 @@ def test_total_order_planner(): [expr('LeftSock'), expr('LeftShoe'), expr('RightSock'), expr('RightShoe')], [expr('RightSock'), expr('RightShoe'), expr('LeftSock'), expr('LeftShoe')] ] - assert TotalOrderPlanner(ss).execute() in possible_solutions + assert Linearize(ss).execute() in possible_solutions def test_expand_actions(): From d8a6697bcebcb8721330be2bcd750ba835c79b58 Mon Sep 17 00:00:00 2001 From: ad71 Date: Sun, 17 Jun 2018 13:41:43 +0530 Subject: [PATCH 19/19] Renamed PDDL to PlanningProblem --- planning.ipynb | 56 ++++++++++++++++---------------- planning.py | 74 +++++++++++++++++++++--------------------- tests/test_planning.py | 4 +-- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/planning.ipynb b/planning.ipynb index e270a7bf6..ca54bcde2 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -19,7 +19,7 @@ "This notebook uses implementations from the [planning.py](https://github.com/aimacode/aima-python/blob/master/planning.py) module. \n", "See the [intro notebook](https://github.com/aimacode/aima-python/blob/master/intro.ipynb) for instructions.\n", "\n", - "We'll start by looking at `PDDL` and `Action` data types for defining problems and actions. \n", + "We'll start by looking at `PlanningProblem` and `Action` data types for defining problems and actions. \n", "Then, we will see how to use them by trying to plan a trip from *Sibiu* to *Bucharest* across the familiar map of Romania, from [search.ipynb](https://github.com/aimacode/aima-python/blob/master/search.ipynb) \n", "followed by some common planning problems and methods of solving them.\n", "\n", @@ -45,7 +45,7 @@ "## CONTENTS\n", "\n", "**Classical Planning**\n", - "- PDDL\n", + "- PlanningProblem\n", "- Action\n", "- Planning Problems\n", " * Air cargo problem\n", @@ -75,10 +75,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## PDDL\n", + "## PlanningProblem\n", "\n", "PDDL stands for Planning Domain Definition Language.\n", - "The `PDDL` class is used to represent planning problems in this module. The following attributes are essential to be able to define a problem:\n", + "The `PlanningProblem` class is used to represent planning problems in this module. The following attributes are essential to be able to define a problem:\n", "* an initial state\n", "* a set of goals\n", "* a set of viable actions that can be executed in the search space of the problem\n", @@ -180,9 +180,9 @@ "\n", "

\n", "\n", - "
class PDDL:\n",
+       "
class PlanningProblem:\n",
        "    """\n",
-       "    Planning Domain Definition Language (PDDL) used to define a search problem.\n",
+       "    Planning Domain Definition Language (PlanningProblem) used to define a search problem.\n",
        "    It stores states in a knowledge base consisting of first order logic statements.\n",
        "    The conjunction of these logical statements completely defines a state.\n",
        "    """\n",
@@ -242,7 +242,7 @@
     }
    ],
    "source": [
-    "psource(PDDL)"
+    "psource(PlanningProblem)"
    ]
   },
   {
@@ -377,7 +377,7 @@
        "
class Action:\n",
        "    """\n",
        "    Defines an action schema using preconditions and effects.\n",
-       "    Use this to describe actions in PDDL.\n",
+       "    Use this to describe actions in PlanningProblem.\n",
        "    action is an Expr where variables are given as arguments(args).\n",
        "    Precondition and effect are both lists with positive and negative literals.\n",
        "    Negative preconditions and effects are defined by adding a 'Not' before the name of the clause\n",
@@ -706,7 +706,7 @@
    },
    "outputs": [],
    "source": [
-    "prob = PDDL(knowledge_base, goals, [fly_s_b, fly_b_s, fly_s_c, fly_c_s, fly_b_c, fly_c_b, drive])"
+    "prob = PlanningProblem(knowledge_base, goals, [fly_s_b, fly_b_s, fly_s_c, fly_c_s, fly_b_c, fly_c_b, drive])"
    ]
   },
   {
@@ -847,7 +847,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    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)', \n",
+       "    return PlanningProblem(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)', \n",
        "                goals='At(C1, JFK) & At(C2, SFO)',\n",
        "                actions=[Action('Load(c, p, a)', \n",
        "                                precond='At(c, a) & At(p, a) & Cargo(c) & Plane(p) & Airport(a)',\n",
@@ -938,7 +938,7 @@
    "metadata": {},
    "source": [
     "It returns False because the goal state is not yet reached. Now, we define the sequence of actions that it should take in order to achieve the goal.\n",
-    "The actions are then carried out on the `airCargo` PDDL.\n",
+    "The actions are then carried out on the `airCargo` PlanningProblem.\n",
     "\n",
     "The actions available to us are the following: Load, Unload, Fly\n",
     "\n",
@@ -1132,7 +1132,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',\n",
+       "    return PlanningProblem(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',\n",
        "                goals='At(Spare, Axle) & At(Flat, Ground)',\n",
        "                actions=[Action('Remove(obj, loc)',\n",
        "                                precond='At(obj, loc)',\n",
@@ -1214,7 +1214,7 @@
    "source": [
     "As we can see, it hasn't completed the goal. \n",
     "We now define a possible solution that can help us reach the goal of having a spare tire mounted onto the car's axle. \n",
-    "The actions are then carried out on the `spareTire` PDDL.\n",
+    "The actions are then carried out on the `spareTire` PlanningProblem.\n",
     "\n",
     "The actions available to us are the following: Remove, PutOn\n",
     "\n",
@@ -1460,7 +1460,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',\n",
+       "    return PlanningProblem(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',\n",
        "                goals='On(A, B) & On(B, C)',\n",
        "                actions=[Action('Move(b, x, y)',\n",
        "                                precond='On(b, x) & Clear(b) & Clear(y) & Block(b) & Block(y)',\n",
@@ -1542,7 +1542,7 @@
    "source": [
     "As we can see, it hasn't completed the goal. \n",
     "We now define a sequence of actions that can stack three blocks in the required order. \n",
-    "The actions are then carried out on the `threeBlockTower` PDDL.\n",
+    "The actions are then carried out on the `threeBlockTower` PlanningProblem.\n",
     "\n",
     "The actions available to us are the following: MoveToTable, Move\n",
     "\n",
@@ -1722,7 +1722,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)',\n",
+       "    return PlanningProblem(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)',\n",
        "                goals='On(B, A) & On(C, B)',\n",
        "                actions=[Action('ToTable(x, y)',\n",
        "                                precond='On(x, y) & Clear(x)',\n",
@@ -1809,7 +1809,7 @@
    "source": [
     "As we can see, it hasn't completed the goal. \n",
     "We now define a sequence of actions that can stack three blocks in the required order. \n",
-    "The actions are then carried out on the `simple_bw` PDDL.\n",
+    "The actions are then carried out on the `simple_bw` PlanningProblem.\n",
     "\n",
     "The actions available to us are the following: MoveToTable, Move\n",
     "\n",
@@ -1998,7 +1998,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',\n",
+       "    return PlanningProblem(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',\n",
        "                goals='Have(Milk) & Have(Banana) & Have(Drill)', \n",
        "                actions=[Action('Buy(x, store)',\n",
        "                                precond='At(store) & Sells(store, x)',\n",
@@ -2086,7 +2086,7 @@
    "metadata": {},
    "source": [
     "We now define a valid solution that will help us reach the goal.\n",
-    "The sequence of actions will then be carried out onto the `shoppingProblem` PDDL."
+    "The sequence of actions will then be carried out onto the `shoppingProblem` PlanningProblem."
    ]
   },
   {
@@ -2273,7 +2273,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='',\n",
+       "    return PlanningProblem(init='',\n",
        "                goals='RightShoeOn & LeftShoeOn',\n",
        "                actions=[Action('RightShoe',\n",
        "                                precond='RightSockOn',\n",
@@ -2359,7 +2359,7 @@
    "metadata": {},
    "source": [
     "As the goal state isn't reached, we will define a sequence of actions that might help us achieve the goal.\n",
-    "These actions will then be acted upon the `socksShoes` PDDL to check if the goal state is reached."
+    "These actions will then be acted upon the `socksShoes` PlanningProblem to check if the goal state is reached."
    ]
   },
   {
@@ -2537,7 +2537,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='Have(Cake)',\n",
+       "    return PlanningProblem(init='Have(Cake)',\n",
        "                goals='Have(Cake) & Eaten(Cake)',\n",
        "                actions=[Action('Eat(Cake)',\n",
        "                                precond='Have(Cake)',\n",
@@ -2623,7 +2623,7 @@
    "metadata": {},
    "source": [
     "We now define a valid solution that can help us reach the goal.\n",
-    "The sequence of actions will then be acted upon the `cakeProblem` PDDL."
+    "The sequence of actions will then be acted upon the `cakeProblem` PlanningProblem."
    ]
   },
   {
@@ -3409,7 +3409,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Given a planning problem defined as a PDDL, `GraphPlan` creates a planning graph stored in `graph` and expands it till it reaches a state where all its required goals are present simultaneously without mutual exclusivity.\n",
+    "Given a planning problem defined as a PlanningProblem, `GraphPlan` creates a planning graph stored in `graph` and expands it till it reaches a state where all its required goals are present simultaneously without mutual exclusivity.\n",
     "
\n", "Once a goal is found, `extract_solution` is called.\n", "This method recursively finds the path to a solution given a planning graph.\n", @@ -4879,8 +4879,8 @@ "## PLANNING IN THE REAL WORLD\n", "---\n", "## PROBLEM\n", - "The `Problem` class is a wrapper for `PDDL` with some additional functionality and data-structures to handle real-world planning problems that involve time and resource constraints.\n", - "The `Problem` class includes everything that the `PDDL` class includes.\n", + "The `Problem` class is a wrapper for `PlanningProblem` with some additional functionality and data-structures to handle real-world planning problems that involve time and resource constraints.\n", + "The `Problem` class includes everything that the `PlanningProblem` class includes.\n", "Additionally, it also includes the following attributes essential to define a real-world planning problem:\n", "- a list of `jobs` to be done\n", "- a dictionary of `resources`\n", @@ -4985,7 +4985,7 @@ "\n", "

\n", "\n", - "
class Problem(PDDL):\n",
+       "
class Problem(PlanningProblem):\n",
        "    """\n",
        "    Define real-world problems by aggregating resources as numerical quantities instead of\n",
        "    named entities.\n",
@@ -5738,7 +5738,7 @@
        "    >>>\n",
        "    """\n",
        "\n",
-       "    return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',\n",
+       "    return PlanningProblem(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',\n",
        "                             goals='Returned(Ball) & At(x, LeftNet) & At(y, RightNet)',\n",
        "                             actions=[Action('Hit(actor, Ball, loc)',\n",
        "                                             precond='Approaching(Ball, loc) & At(actor, loc)',\n",
diff --git a/planning.py b/planning.py
index 77406cffb..9492e2c8b 100644
--- a/planning.py
+++ b/planning.py
@@ -10,9 +10,9 @@
 from functools import reduce as _reduce
 
 
-class PDDL:
+class PlanningProblem:
     """
-    Planning Domain Definition Language (PDDL) used to define a search problem.
+    Planning Domain Definition Language (PlanningProblem) 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 defines a state.
     """
@@ -64,7 +64,7 @@ def act(self, action):
 class Action:
     """
     Defines an action schema using preconditions and effects.
-    Use this to describe actions in PDDL.
+    Use this to describe actions in PlanningProblem.
     action is an Expr where variables are given as arguments(args).
     Precondition and effect are both lists with positive and negative literals.
     Negative preconditions and effects are defined by adding a 'Not' before the name of the clause
@@ -187,7 +187,7 @@ def air_cargo():
     >>>
     """
 
-    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)', 
+    return PlanningProblem(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)',
@@ -221,7 +221,7 @@ def spare_tire():
     >>>
     """
 
-    return PDDL(init='Tire(Flat) & Tire(Spare) & At(Flat, Axle) & At(Spare, Trunk)',
+    return PlanningProblem(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)',
@@ -257,7 +257,7 @@ def three_block_tower():
     >>>
     """
 
-    return PDDL(init='On(A, Table) & On(B, Table) & On(C, A) & Block(A) & Block(B) & Block(C) & Clear(B) & Clear(C)',
+    return PlanningProblem(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)',
@@ -288,7 +288,7 @@ def simple_blocks_world():
     >>>
     """
 
-    return PDDL(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)',
+    return PlanningProblem(init='On(A, B) & Clear(A) & OnTable(B) & OnTable(C) & Clear(C)',
                 goals='On(B, A) & On(C, B)',
                 actions=[Action('ToTable(x, y)',
                                 precond='On(x, y) & Clear(x)',
@@ -320,7 +320,7 @@ def have_cake_and_eat_cake_too():
     >>>
     """
 
-    return PDDL(init='Have(Cake)',
+    return PlanningProblem(init='Have(Cake)',
                 goals='Have(Cake) & Eaten(Cake)',
                 actions=[Action('Eat(Cake)',
                                 precond='Have(Cake)',
@@ -353,7 +353,7 @@ def shopping_problem():
     >>>
     """
 
-    return PDDL(init='At(Home) & Sells(SM, Milk) & Sells(SM, Banana) & Sells(HW, Drill)',
+    return PlanningProblem(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)',
@@ -385,7 +385,7 @@ def socks_and_shoes():
     >>>
     """
 
-    return PDDL(init='',
+    return PlanningProblem(init='',
                 goals='RightShoeOn & LeftShoeOn',
                 actions=[Action('RightShoe',
                                 precond='RightSockOn',
@@ -423,7 +423,7 @@ def double_tennis_problem():
     >>>
     """
 
-    return PDDL(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',
+    return PlanningProblem(init='At(A, LeftBaseLine) & At(B, RightNet) & Approaching(Ball, RightBaseLine) & Partner(A, B) & Partner(B, A)',
                              goals='Returned(Ball) & At(a, LeftNet) & At(a, RightNet)',
                              actions=[Action('Hit(actor, Ball, loc)',
                                              precond='Approaching(Ball, loc) & At(actor, loc)',
@@ -570,9 +570,9 @@ class Graph:
     Used in graph planning algorithm to extract a solution
     """
 
-    def __init__(self, pddl):
-        self.pddl = pddl
-        self.kb = FolKB(pddl.init)
+    def __init__(self, planningproblem):
+        self.planningproblem = planningproblem
+        self.kb = FolKB(planningproblem.init)
         self.levels = [Level(self.kb)]
         self.objects = set(arg for clause in self.kb.clauses for arg in clause.args)
 
@@ -583,7 +583,7 @@ def expand_graph(self):
         """Expands the graph by a level"""
 
         last_level = self.levels[-1]
-        last_level(self.pddl.actions, self.objects)
+        last_level(self.planningproblem.actions, self.objects)
         self.levels.append(last_level.perform_actions())
 
     def non_mutex_goals(self, goals, index):
@@ -603,8 +603,8 @@ class GraphPlan:
     Returns solution for the planning problem
     """
 
-    def __init__(self, pddl):
-        self.graph = Graph(pddl)
+    def __init__(self, planningproblem):
+        self.graph = Graph(planningproblem)
         self.nogoods = []
         self.solution = []
 
@@ -677,15 +677,15 @@ 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)
+        return all(kb.ask(q) is not False for q in self.graph.planningproblem.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 (self.goal_test(self.graph.levels[-1].kb) and self.graph.non_mutex_goals(self.graph.planningproblem.goals, -1)):
+                solution = self.extract_solution(self.graph.planningproblem.goals, -1)
                 if solution:
                     return solution
             
@@ -695,8 +695,8 @@ def execute(self):
 
 class Linearize:
 
-    def __init__(self, pddl):
-        self.pddl = pddl
+    def __init__(self, planningproblem):
+        self.planningproblem = planningproblem
 
     def filter(self, solution):
         """Filter out persistence actions from a solution"""
@@ -710,11 +710,11 @@ def filter(self, solution):
             new_solution.append(new_section)
         return new_solution
 
-    def orderlevel(self, level, pddl):
+    def orderlevel(self, level, planningproblem):
         """Return valid linear order of actions for a given level"""
 
         for permutation in itertools.permutations(level):
-            temp = copy.deepcopy(pddl)
+            temp = copy.deepcopy(planningproblem)
             count = 0
             for action in permutation:
                 try:
@@ -722,7 +722,7 @@ def orderlevel(self, level, pddl):
                     count += 1
                 except:
                     count = 0
-                    temp = copy.deepcopy(pddl)
+                    temp = copy.deepcopy(planningproblem)
                     break
             if count == len(permutation):
                 return list(permutation), temp
@@ -731,12 +731,12 @@ def orderlevel(self, level, pddl):
     def execute(self):
         """Finds total-order solution for a planning graph"""
 
-        graphplan_solution = GraphPlan(self.pddl).execute()
+        graphplan_solution = GraphPlan(self.planningproblem).execute()
         filtered_solution = self.filter(graphplan_solution)
         ordered_solution = []
-        pddl = self.pddl
+        planningproblem = self.planningproblem
         for level in filtered_solution:
-            level_solution, pddl = self.orderlevel(level, pddl)
+            level_solution, planningproblem = self.orderlevel(level, planningproblem)
             for element in level_solution:
                 ordered_solution.append(element)
 
@@ -779,15 +779,15 @@ def linearize(solution):
 
 class PartialOrderPlanner:
 
-    def __init__(self, pddl):
-        self.pddl = pddl
+    def __init__(self, planningproblem):
+        self.planningproblem = planningproblem
         self.initialize()
 
     def initialize(self):
         """Initialize all variables"""
         self.causal_links = []
-        self.start = Action('Start', [], self.pddl.init)
-        self.finish = Action('Finish', self.pddl.goals, [])
+        self.start = Action('Start', [], self.planningproblem.init)
+        self.finish = Action('Finish', self.planningproblem.goals, [])
         self.actions = set()
         self.actions.add(self.start)
         self.actions.add(self.finish)
@@ -801,15 +801,15 @@ def initialize(self):
     def expand_actions(self, name=None):
         """Generate all possible actions with variable bindings for precondition selection heuristic"""
 
-        objects = set(arg for clause in self.pddl.init for arg in clause.args)
+        objects = set(arg for clause in self.planningproblem.init for arg in clause.args)
         expansions = []
         action_list = []
         if name is not None:
-            for action in self.pddl.actions:
+            for action in self.planningproblem.actions:
                 if str(action.name) == name:
                     action_list.append(action)
         else:
-            action_list = self.pddl.actions
+            action_list = self.planningproblem.actions
 
         for action in action_list:
             for permutation in itertools.permutations(objects, len(action.args)):
@@ -893,7 +893,7 @@ def find_action_for_precondition(self, oprec):
 
         # or
         #   choose act0 E Actions such that act0 achieves G
-        for action in self.pddl.actions:
+        for action in self.planningproblem.actions:
             for effect in action.effect:
                 if effect.op == oprec.op:
                     bindings = unify(effect, oprec)
@@ -1225,7 +1225,7 @@ def inorder(self, job_order):
         return True
 
 
-class Problem(PDDL):
+class Problem(PlanningProblem):
     """
     Define real-world problems by aggregating resources as numerical quantities instead of
     named entities.
diff --git a/tests/test_planning.py b/tests/test_planning.py
index 926a76671..5b6943ee3 100644
--- a/tests/test_planning.py
+++ b/tests/test_planning.py
@@ -117,8 +117,8 @@ def test_shopping_problem():
 
 
 def test_graph_call():
-    pddl = spare_tire()
-    graph = Graph(pddl)
+    planningproblem = spare_tire()
+    graph = Graph(planningproblem)
 
     levels_size = len(graph.levels)
     graph()