Implementing a well-performed planning algorithm seems complex, how do we get it start?

Well, PDDL, the Planning Domain Definition Language, describes four things we need to define a search problem:

The initial state The actions that are available in a state The result of applying an action The goal state for goal test

So we decompose the problem of implementing planning algorithm into 2 sub-problems:

A search algorithm that can find a solution for a given problem, which also defines search problem interface. A PDDL search problem that interprets our PDDL representation.

Then, we choose a relatively simple PDDL problem that fits the following conditions:

It has small search space, so that we can start with a simple search algorithm. It is a propositional planning problem, which has no variables defined in the action, so that we can start with a simplified version of PDDL.

Considering all problem examples in the Artificial Intelligence: A Modern Approach (AIMA) book, the Cake problem seems a good fit. Here is the Cake problem described by our PDDL in Ruby (see previous post Classical Planning: #1 PDDL):

{ init : [[ :have , :cake ]] , goal : [[ :have , :cake ] , [ :eaten , :cake ]] , solution : [[ :eat ] , [ :bake ]] actions : [ { name : :eat , precond : [[ :have , :cake ]] , effect : [[ :- , :have , :cake ] , [ :eaten , :cake ]] }, { name : :bake , precond : [[ :- , :have , :cake ]] , effect : [[ :have , :cake ]] } ] }

(Notice: the Cake problem is from PLANNING GRAPHS section, but we won’t touch planning graph today)

I imbedded solution in above problem description for keeping related information together.

Our first test will look like this:

result = Longjing . plan ( cake_problem ) assert ! result [ :solution ]. nil? assert_equal problem [ :solution ] , result [ :solution ]

Passing this test is today’s goal. We’ll use Child Test strategy described in Test-Driven Development by Example book to reach the goal.

The search algorithm and problem interface

As we discussed above, the first search algorithm we need implement can be simple. So we’ll implement a classical search discussed in chapter 3 “Solving Problems by Searching”of the AIMA book.

Considering graph search is just little bit more complex than tree search, we’ll just go for graph search. For search strategy, we’ll pick breadth-first. Here is new test:

def test_breadth_first_search search = Longjing :: Search . new ( :breadth_first ) @goal = :g ret = search . resolve ( self ) assert_equal [[ :move , :a , :d ] , [ :move , :d , :g ]] , ret [ :solution ] assert_equal [ :h ] , ret [ :frontier ] assert_equal [ :a , :b , :c , :d , :e , :f , :g ] , ret [ :explored ] end

In this test, we’re not only caring about the final solution, but also describing the frontier and explored states, so that we know the algorithm used for resolving the problem is the one we expected.

We use TDD strategy Self Shunt to test search algorithm communicates correctly with problem interface. So here is problem interface defined in test:

# problem interface # ret: state def initial :a end # ret: boolean def goal? ( state ) @goal == state end # ret: [action] def actions ( state ) map = { a : [ :b , :c , :d ] , b : [ :a ] , c : [ :e , :f ] , d : [ :g ] , e : [ :h ] , f : [ :h ] , g : [ :h ] , h : [] } map [ state ]. map { | to | { name : :move , to : to }} end # ret: [action-description] def describe ( action , state ) [ action [ :name ] , state , action [ :to ]] end # ret: state def result ( action , state ) action [ :to ] end

As we expect solution to be a sequence of events [[:move, :a, :d], [:move, :d, :g]] instead of states path, we define an interface method describe(action, state) for converting an exploring event (applying an action to a state) into expected format defined by the problem. (Technically say: we’re hiding data structure of action and state from search algorithm, as problem defines & operates them)

The problem we defined in this test for interacting with search algorithm is a route-finding problem. We define it by symbols along links between them using a Hash in actions(state) method. The following directed-graph describes the map:

The implementation of search is similar with the graph search described in the chapter 3 “Solving Problems by Searching” of AIMA book, except we construct event sequence as solution. Hence we have a sequences hash to store all sequences of explored and frontier states:

def resolve ( problem ) sequences = {} frontier , explored = [ problem . initial ] , Set . new solution = nil until frontier . empty? do state = @strategy [ frontier , sequences ] explored << state if problem . goal? ( state ) solution = Array ( sequences [ state ] ) break end problem . actions ( state ) . each do | action | new_state = problem . result ( action , state ) if ! explored . include? ( new_state ) && ! frontier . include? ( new_state ) frontier << new_state sequence = Array ( sequences [ state ] ) sequences [ new_state ] = sequence + [ problem . describe ( action , state ) ] end end end { :frontier => frontier , :explored => explored . to_a , :solution => solution } end

The breadth-first strategy implementation is also straightforward; as sequences hash has all state sequences, we use it to find out next exploring state having shortest path in frontier:

STRATEGIES = { :breadth_first => lambda do | frontier , sequences | frontier . sort_by! do | state | if seq = sequences [ state ] seq . size else 0 end end . shift end }

Full implementations:

The PDDL search problem

Now we have clear interface of search problem. PDDL is just another search problem that follows same protocol.

Implementing initial and goal test goal?(state) methods are relatively simple.

Test:

def test_initial prob = Longjing . problem ( cake_problem ) assert_equal [[ :have , :cake ]] , prob . initial end def test_goal_test prob = Longjing . problem ( cake_problem ) assert prob . goal? ( [[ :have , :cake ] , [ :eaten , :cake ]] ) assert prob . goal? ( [[ :eaten , :cake ] , [ :have , :cake ]] ) assert ! prob . goal? ( [[ :eaten , :cake ]] ) assert ! prob . goal? ( [[ :have , :cake ]] ) end

(Notice here [:have, :cake] is just one part of whole state. We simply use an Array to represent a state, so a state only contains [:have, :cake] is [[:have, :cake]] .)

Implementation:

# longjing.rb module Longjing module_function def problem ( data ) Problem . new ( data ) end # longjing/problem.rb class Problem attr_reader :initial def initialize ( data ) @initial = data [ :init ] @goal = data [ :goal ]. sort @actions = data [ :actions ] end def goal? ( state ) @goal == state . sort end

The complexity of PDDL search problem is coming from interpreting action schema in actions(state) and result(action, state) methods. We start with a test like this:

def test_actions prob = Longjing . problem ( cake_problem ) actions = prob . actions ( [[ :have , :cake ]] ) assert_equal 1 , actions . size assert_equal :eat , actions [ 0 ][ :name ] end

Then we’ll look for actions precondition of that matches given state; the only special condition we need handle is negative condition, which starts with :- :

def actions ( state ) @actions . select do | action | action [ :precond ]. all? do | cond | case cond [ 0 ] when :- ! state . include? ( cond [ 1 . . - 1 ] ) else state . include? ( cond ) end end end end

(An interesting fact I found in Ruby (2.2): using Array#[1..-1] to get [2, 3] from [1, 2, 3] is faster than using Array#[1] to get [2, 3] from [1, [2, 3]] .)

Similarly, here is result(action, state) test to kick off implementing Problem#result:

def test_result prob = Longjing . problem ( cake_problem ) state = [[ :have , :cake ]] actions = prob . actions ( state ) ret = prob . result ( actions [ 0 ] , state ) assert_equal [[ :eaten , :cake ]] , ret end

The result state should be removing negative literals and adding positive literals in the action’s effects:

def result ( action , state ) action [ :effect ]. inject ( state ) do | memo , effect | case effect [ 0 ] when :- memo - [ effect [ 1 . . - 1 ]] else memo . include? ( effect ) ? memo : memo + [ effect ] end end end

The last method describe(action, state) is also simple, as we don’t have variables defined in action yet.

Test:

def test_describe prob = Longjing . problem ( cake_problem ) state = [[ :have , :cake ]] actions = prob . actions ( state ) assert_equal [ :eat ] , prob . describe ( actions [ 0 ] , state ) end

Implementation:

def describe ( action , state ) [ action [ :name ]] end

Full implementations:

Back to Cake problem

After we sorted out the PDDL search problem, we just need a little more work to hook up everything for solving Cake problem (test passes):

module Longjing module_function . . . def plan ( problem ) Search . new ( :breadth_first ) . resolve ( self . problem ( problem )) end end

What’s Next…

Now we have a minimum PDDL planner working, the next step is expending action schema to support variables for describing more complex problems. We’ll aim for solving Blocks World problem in next post and probably start to discovery heuristic function for A* search.