Getting Started
Welcome to using PDDL.jl! This tutorial covers how to install PDDL.jl, how to load your first domain and problem, how to manipulate and inspect states and actions, and how to write and execute a plan that achieves a goal.
Installation
First, download and run Julia, available here (version 1.3 or later required). Optionally, create your own project and activate its environment. Next, press ] in the Julia REPL to enter the package manager, then install the registered version of PDDL.jl by running:
add PDDLTo install the latest development version, you may instead run:
add https://github.com/JuliaPlanners/PDDL.jl.gitPDDL.jl can now be used in the Julia REPL, or at the top of a script:
using PDDLLoading Domains and Problems
PDDL stands for the Planning Domain Definition Language, a formal language for specifying the semantics of planning domains and problems. PDDL domain and problem definitions are typically saved as text files with the .pddl extension.
Loading Domains
A PDDL domain defines the high-level "physics" or transition dynamics of a planning task. A classic example is Blocksworld, a domain where blocks may be stacked on top of each other, or placed on a table:
(define (domain blocksworld)
(:requirements :strips :typing :equality)
(:types block)
(:predicates (on ?x ?y - block) (ontable ?x - block) (clear ?x - block)
(handempty) (holding ?x - block))
(:action pick-up
:parameters (?x - block)
:precondition (and (clear ?x) (ontable ?x) (handempty))
:effect (and (not (ontable ?x)) (not (clear ?x))
(not (handempty)) (holding ?x)))
(:action put-down
:parameters (?x - block)
:precondition (holding ?x)
:effect (and (not (holding ?x)) (clear ?x)
(handempty) (ontable ?x)))
(:action stack
:parameters (?x ?y - block)
:precondition (and (holding ?x) (clear ?y) (not (= ?x ?y)))
:effect (and (not (holding ?x)) (not (clear ?y)) (clear ?x)
(handempty) (on ?x ?y)))
(:action unstack
:parameters (?x ?y - block)
:precondition (and (on ?x ?y) (clear ?x) (handempty) (not (= ?x ?y)))
:effect (and (holding ?x) (clear ?y) (not (clear ?x))
(not (handempty)) (not (on ?x ?y))))
)Suppose this domain definition is saved in a file named blocksworld.pddl in the current directory. After loading PDDL.jl with using PDDL, we can load the Blocksworld domain by calling load_domain:
domain = load_domain("blocksworld.pddl")We can then inspect the name of domain, and the list of action names:
julia> PDDL.get_name(domain)
:blocksworld
julia> PDDL.get_actions(domain) |> keys .|> string
4-element Vector{String}:
"pick-up"
"unstack"
"put-down"
"stack"Loading Problems
PDDL domains only define the general semantics of the planning task that apply across any set of objects or goals. To fully define a planning task, we also need to load a PDDL problem, which defines an initial state, and a goal to be achieved:
(define (problem blocksworld-problem)
(:domain blocksworld)
(:objects a b c - block)
(:init (handempty) (ontable a) (ontable b) (ontable c)
(clear a) (clear b) (clear c))
(:goal (and (clear c) (ontable b) (on c a) (on a b)))
)In this problem, there are 3 blocks, a, b, and c, which are all initially placed on the table (ontable), with no other blocks placed on them (clear). The goal is to stack the blocks such that c is on a is on b.
Suppose the problem definition is saved in blocksworld-problem.pddl. We can load it by calling load_problem:
problem = load_problem("blocksworld-problem.pddl")We can then inspect the list of objects, and the goal to be reached:
julia> PDDL.get_objects(problem) |> println
Const[a, b, c]
julia> PDDL.get_goal(problem) |> write_pddl
"(and (clear c) (ontable b) (on c a) (on a b))"Loading From A Repository
A wide variety of standard PDDL domains and problems can be found online, such as this repository of instances from the International Planning Competition (IPC). To ease the (down)loading of these domains and problems, the PDDL.jl ecosystem includes PlanningDomains.jl, which contains both a built-in repository of domains and problems, and an interface for accessing domains and problems from other online repositories.
PlanningDomains.jl can be installed from the Pkg REPL as per usual:
add PlanningDomainsOnce installed, we can use PlanningDomains.jl to directly load Blocksworld domains and problems:
using PlanningDomains
domain = load_domain(:blocksworld)
problem = load_problem(:blocksworld, "problem-2")We can also specify external repositories to download from, such as the previously mentioned repository of IPC domains and problems:
domain = load_domain(IPCInstancesRepo, "ipc-2000", "blocks-strips-typed")
problem = load_problem(IPCInstancesRepo, "ipc-2000", "blocks-strips-typed", "problem-2")Constructing and Inspecting States
Now that we've loaded a domain and problem, we can construct the initial state (specified by the problem file) using the initstate function:
state = initstate(domain, problem)Inspecting Facts and Relations
Conceptually, a state consists of a set of objects, and a set of true facts and relations about those objects. We can list the set of facts using PDDL.get_facts:
julia> PDDL.get_facts(state)
Set{Term} with 7 elements:
clear(a)
ontable(b)
clear(b)
handempty
ontable(a)
ontable(c)
clear(c)Facts are printed in Prolog-style syntax by default: ontable(a) in Prolog is the same as (ontable a) in PDDL. This is because PDDL.jl uses Julog.jl to represent terms and expressions in first-order logic.
In addition to listing facts, we can query the truth value of specific terms using the satisfy function:
julia> satisfy(domain, state, pddl"(ontable a)")
true
julia> satisfy(domain, state, pddl"(on a b)")
falseHere, we used the pddl"..." string macro to construct a first-order Term. This allows us to write pddl"(on a b)" as syntactic sugar for the expression Compound(:on, Term[Const(:a), Const(:b)]). (It is also possible to interpolate values when using the pddl"..." macro.)
Besides querying whether particular terms are true or false, we can also ask PDDL.jl to return all satisfying assignments to a logical formula with free variables using the satisfiers function:
julia> satisfiers(domain, state, pddl"(and (ontable ?x) (clear ?x))")
3-element Vector{Any}:
{X => b}
{X => a}
{X => c}Our query pddl"(and (ontable ?x) (clear ?x))" expresses that some object ?x is on the table, and is clear (i.e. has no other blocks on top of it), where ?x is PDDL syntax for a variable in a first-order formula. Since blocks a, b and c all satisfy the query, satisfiers returns a list of corresponding variable substitutions. Note that the PDDL variable ?x gets rendered in Prolog-style syntax as a capital X, by the convention in Prolog that capital letters refer to variables.
Inspecting Non-Boolean Fluents
PDDL is not limited to domains where object properties and relations must have Boolean values. For example, the Zeno Travel domain includes numeric properties and relations, such as the distance between two cities, or the amount of fuel in a plane. We can construct and inspect a state in this domain as well:
zt_domain = load_domain(:zeno_travel)
zt_problem = load_problem(:zeno_travel, "problem-1")
zt_state = initstate(zt_domain, zt_problem)To inspect all properties and relations (Boolean or otherwise) in this state, we can iterate over the list of pairs returned by PDDL.get_fluents:
julia> PDDL.get_fluents(zt_state) |> collect
13-element Vector{Pair}:
at(plane1, city0) => true
at(person1, city0) => true
onboard(plane1) => 0
slow-burn(plane1) => 4
⋮
fuel(plane1) => 3956
fast-burn(plane1) => 15
zoom-limit(plane1) => 8
capacity(plane1) => 10232These properties and relations are called fluents, a term historically used in AI research to describe facts about the world that may change over time.
Fluents are sometimes also called "state variables", but we avoid that terminology to prevent confusion with variables in the context of first-order terms and formulae. In keeping with the terminology of first-order logic, Boolean fluents such as (at ?plane ?city) are also called predicates, and non-Boolean fluents such as (fuel ?plane) are called functions (because they map objects to values).
For conciseness, some implementations of the PDDL.jl interface will omit predicates that are false from the list returned by PDDL.get_fluents, as is the case above.
In addition to listing fluents, we can evaluate specific fluents using the evaluate function. Below, we query the amount of fuel in plane1:
julia> evaluate(zt_domain, zt_state, pddl"(fuel plane1)")
3956We can also evaluate compound expressions of multiple fluents. For example, we might be curious to know the amount of additional fuel that plane1 can hold. As syntactic sugar for evaluate(domain, state, term), we can also use the syntax domain[state => term]:
julia> evaluate(zt_domain, zt_state, pddl"(- (capacity plane1) (fuel plane1))")
6276
julia> zt_domain[zt_state => pddl"(- (capacity plane1) (fuel plane1))"]
6276For non-compound expressions stored directly in the state, we can use PDDL.get_fluent to look up the value of a term in state, or state[term] for short:
julia> state[pddl"(on a b)"] # Blocksworld query
false
julia> zt_state[pddl"(fuel plane1)"] # Zeno Travel query
3956Inspecting Objects and Object Types
Since PDDL states consist of sets of (optionally typed) objects, PDDL.jl provides the PDDL.get_objects function to list all objects in a state, as well as all objects of particular type:
julia> PDDL.get_objects(state) |> println # Blocksworld objects
Const[c, a, b]
julia> PDDL.get_objects(zt_state, :aircraft) |> println # Zeno Travel aircraft
Const[plane1]
julia> PDDL.get_objects(zt_domain, zt_state, :movable) |> println # Zeno Travel movables
Const[person1, plane1]Note that in the third call to PDDL.get_objects, we also provided the domain as the first argument. This is because the domain stores information about the type hierarchy, and the movable type in the Zeno Travel domain is abstract: There are no objects in the state which have the type movable. There only objects of its subtypes, person and aircraft. We can inspect the type hierarchy of a domain using PDDL.get_typetree:
julia> PDDL.get_typetree(zt_domain)
Dict{Symbol, Vector{Symbol}} with 5 entries:
:object => [:movable, :city]
:movable => [:aircraft, :person]
:aircraft => []
:person => []
:city => []Finally, we can inspect the type of a specific object using PDDL.get_objtype:
julia> PDDL.get_objtype(zt_state, pddl"(person1)")
:personExecuting Actions and Plans
PDDL domains not only define the predicates and functions which describe a state, but also a set of actions which can modify a state. Having learned how to inspect the contents of a state, we can now modify them using actions.
Instantiating Actions
In PDDL and symbolic planning more broadly, we distinguish between action schemas (also known as operators), which specify the general semantics of an action, and ground actions, which represent instantiations of actions for specific objects. We can inspect the definition of an action schema in a domain using PDDL.get_action, such as the definition of stack below:
julia> PDDL.get_action(domain, :stack) |> write_pddl |> print
(:action stack
:parameters (?x ?y - block)
:precondition (and (holding ?x) (clear ?y) (not (= ?x ?y)))
:effect (and (not (holding ?x)) (not (clear ?y)) (clear ?x) (handempty) (on ?x ?y)))The stack schema has two parameters (or arguments) of type block. This means that ground instances of the stack schema have to be applied to two block objects. The schema also specifies a precondition formula, which has to hold true in order for the action to be executable (a.k.a. available) in the current state. Finally, the schema contains an effect formula, which specifies facts that will either be added or deleted in the next state. In domains with non-Boolean fluents, effects may also assign or modify the values of fluents.
To refer to a specific application of this action schema to blocks a and b (i.e., a ground action), we can simply write pddl"(stack a b)", which constructs a Term with stack as its name, and with a and b as arguments:
julia> pddl"(stack a b)" |> dump
Compound
name: Symbol stack
args: Array{Term}((2,))
1: Const
name: Symbol a
2: Const
name: Symbol bIf unspecified, whether we are referring to action schemas or ground actions shall be clear from context.
Listing Available Actions
For our initial state in the Blocksworld domain, we can iterate over the list of available ground actions (i.e. those with satisfied preconditions) using the available function:
julia> available(domain, state) |> collect
3-element Vector{Compound}:
pick-up(a)
pick-up(b)
pick-up(c)Note that available returns an iterator over such actions, so we have to collect this iterator in order to get a Vector result. As before, action Terms are printed in Prolog-style syntax.
Executing Actions
Since we now know which actions are available, we can execute one of them to get another state:
julia> next_state = execute(domain, state, pddl"(pick-up a)");
julia> satisfy(domain, next_state, pddl"(holding a)")
trueWe see that after executing the pddl"(pick-up a)" action, block a is now being held. In contrast, if we try to execute a non-available action, PDDL.jl will throw an error:
julia> next_state = execute(domain, state, pddl"(stack a b)");
ERROR: Precondition (and (holding ?x) (clear ?y) (not (= ?x ?y))) does not hold.
⋮Instead of using execute, we can also use the transition function. For domains written in standard PDDL, these functions have the same behavior, but there are extensions of PDDL which include events and processes that are handled by transition only. Note that both execute and transition do not mutate the original state passed in as an argument. For mutating versions, see execute! and transition!.
Executing and Simulating Plans
Now that we know how to execute an action, we can execute a series of actions (i.e. a plan) to achieve our goal in the Blocksworld domain. We can do this by repeatedly calling transition:
state = initstate(domain, problem)
state = transition(domain, state, pddl"(pick-up a)")
state = transition(domain, state, pddl"(stack a b)")
state = transition(domain, state, pddl"(pick-up c)");
state = transition(domain, state, pddl"(stack c a)");And then check that our goal is indeed satisfied:
julia> goal = PDDL.get_goal(problem) # Our goal is stack `c` on `a` on `b`
and(clear(c), ontable(b), on(c, a), on(a, b))
julia> satisfy(domain, state, goal)
trueRather than repeatedly call transition, we can use the PDDL.simulate function to directly simulate the end result of a sequence of actions:
state = initstate(domain, problem)
plan = @pddl("(pick-up a)", "(stack a b)", "(pick-up c)", "(stack c a)")
end_state = PDDL.simulate(EndStateSimulator(), domain, state, plan)As before, the goal is satisfied in the final state:
julia> satisfy(domain, end_state, goal)
trueThe first argument to PDDL.simulate is a concrete instance of a Simulator, which controls what information is collected as the simulation progresses. By default, the first argument is a StateRecorder, which leads PDDL.simulate to return the trajectory of all states encountered, including the first:
julia> traj = PDDL.simulate(domain, state, plan);
julia> eltype(traj)
GenericState
julia> length(traj)
5You've now learned how to load PDDL domains and problems, construct and inspect states, and execute (sequences of) actions – congratulations! In the next tutorial, you can learn how to write your very own planning algorithms using the functions introduced here.