#!/usr/bin/env python # coding: utf-8 # ## SOLVING PLANNING PROBLEMS # ---- # ### GRAPHPLAN #
# The GraphPlan algorithm is a popular method of solving classical planning problems. # 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. # ### Planning Graph # A planning graph is a directed graph organized into levels. # Each level contains information about the current state of the knowledge base and the possible state-action links to and from that level. # The first level contains the initial state with nodes representing each fluent that holds in that level. # This level has state-action links linking each state to valid actions in that state. # Each action is linked to all its preconditions and its effect states. # Based on these effects, the next level is constructed. # The next level contains similarly structured information about the next state. # In this way, the graph is expanded using state-action links till we reach a state where all the required goals hold true simultaneously. # We can say that we have reached our goal if none of the goal states in the current level are mutually exclusive. # This will be explained in detail later. #
# Planning graphs only work for propositional planning problems, hence we need to eliminate all variables by generating all possible substitutions. #
# For example, the planning graph of the `have_cake_and_eat_cake_too` problem might look like this # ![title](images/cake_graph.jpg) #
# The black lines indicate links between states and actions. #
# In every planning problem, we are allowed to carry out the `no-op` action, ie, we can choose no action for a particular state. # These are called 'Persistence' actions and are represented in the graph by the small square boxes. # In technical terms, a persistence action has effects same as its preconditions. # This enables us to carry a state to the next level. #
#
# The gray lines indicate mutual exclusivity. # This means that the actions connected bya gray line cannot be taken together. # Mutual exclusivity (mutex) occurs in the following cases: # 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)_ # 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. # 3. **Competing needs**: One of the preconditions of one action is mutually exclusive with a precondition of the other. For example, _Bake(Cake)_ and _Eat(Cake)_ are mutex because they compete on the value of the _Have(Cake)_ precondition. # In the module, planning graphs have been implemented using two classes, `Level` which stores data for a particular level and `Graph` which connects multiple levels together. # Let's look at the `Level` class. # In[14]: from planning import * from notebook import psource # In[15]: psource(Level) # Each level stores the following data # 1. The current state of the level in `current_state` # 2. Links from an action to its preconditions in `current_action_links` # 3. Links from a state to the possible actions in that state in `current_state_links` # 4. Links from each action to its effects in `next_action_links` # 5. Links from each possible next state from each action in `next_state_links`. This stores the same information as the `current_action_links` of the next level. # 6. Mutex links in `mutex`. #
#
# The `find_mutex` method finds the mutex links according to the points given above. #
# The `build` method populates the data structures storing the state and action information. # Persistence actions for each clause in the current state are also defined here. # The newly created persistence action has the same name as its state, prefixed with a 'P'. # Let's now look at the `Graph` class. # In[16]: psource(Graph) # The class stores a problem definition in `pddl`, # a knowledge base in `kb`, # a list of `Level` objects in `levels` and # all the possible arguments found in the initial state of the problem in `objects`. #
# The `expand_graph` method generates a new level of the graph. # This method is invoked when the goal conditions haven't been met in the current level or the actions that lead to it are mutually exclusive. # The `non_mutex_goals` method checks whether the goals in the current state are mutually exclusive. #
#
# Using these two classes, we can define a planning graph which can either be used to provide reliable heuristics for planning problems or used in the `GraphPlan` algorithm. #
# Let's have a look at the `GraphPlan` class. # In[17]: psource(GraphPlan) # 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. #
# Once a goal is found, `extract_solution` is called. # This method recursively finds the path to a solution given a planning graph. # 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**. # 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. # No-goods are also used in the termination test. #
# 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. # 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. # In such a case, we can declare that the planning problem is unsolvable with the given constraints. #
#
# To summarize, the `GraphPlan` algorithm calls `expand_graph` and tests whether it has reached the goal and if the goals are non-mutex. #
# If so, `extract_solution` is invoked which recursively reconstructs the solution from the planning graph. #
# If not, then we check if our graph has levelled off and continue if it hasn't. # Let's solve a few planning problems that we had defined earlier. # #### Air cargo problem # In accordance with the summary above, we have defined a helper function to carry out `GraphPlan` on the `air_cargo` problem. # The function is pretty straightforward. # Let's have a look. # In[18]: psource(air_cargo_graphplan) # Let's instantiate the problem and find a solution using this helper function. # In[19]: airCargoG = air_cargo_graphplan() airCargoG # Each element in the solution is a valid action. # The solution is separated into lists for each level. # The actions prefixed with a 'P' are persistence actions and can be ignored. # They simply carry certain states forward. # 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. # In[20]: linearize(airCargoG) # Indeed, this is a correct solution. #
# There are similar helper functions for some other planning problems. #
# Lets' try solving the spare tire problem. # In[21]: spareTireG = spare_tire_graphplan() linearize(spareTireG) # Solution for the cake problem # In[22]: cakeProblemG = have_cake_and_eat_cake_too_graphplan() linearize(cakeProblemG) # Solution for the Sussman's Anomaly configuration of three blocks. # In[23]: sussmanAnomalyG = three_block_tower_graphplan() linearize(sussmanAnomalyG) # Solution of the socks and shoes problem # In[24]: socksShoesG = socks_and_shoes_graphplan() linearize(socksShoesG)