Parsing and Inspecting PDDL problems

Let’s use the built-in PDDL parser to inspect some problem encoded in PDDL:

[1]:
from tarski.io import PDDLReader
reader = PDDLReader(raise_on_error=True)
reader.parse_domain('./benchmarks/blocksworld.pddl')
problem = reader.parse_instance('./benchmarks/probBLOCKS-4-2.pddl')
lang = problem.language

Notice how the parsing of a standard instance of the Blocks world results in a problem object and, within that problem, a language object. There is a clear distinction in Tarski between the language used to define a planning problem, and the problem itself. Tarski sticks as close as possible to the standard definition of many-sorted first-order languages. Hence, languages have a vocabulary made up of predicate and function names, each with their arity and sort, and allow the definition of terms and formulas in the usual manner. Let us inspect the language of the problem we just parsed:

[2]:
print(lang.sorts)
print(lang.predicates)
print(lang.functions)
[Sort(object)]
[=/2, !=/2, on/2, ontable/1, clear/1, handempty/0, holding/1]
[]

Turns out that our blocks encoding has one single sort object (the default sort in PDDL when no sort is declared), no functions, and a few predicates. Besides the predicates defined in the PDDL file, Tarski assumes (unless explicitly disallowed) the existence of a built-in equality predicate and its negation. A string clear/1 denotes a predicate named clear with arity 1.

Constants are usually considered as nullary functions in the literature, but in Tarski they are stored separately:

[3]:
lang.constants()
[3]:
[b (object), d (object), c (object), a (object)]

Our blocks encoding this has four constants of type object, which we know represent the four different blocks in the problem.

Languages also provide means to directly retrieve any sort, constant, function or predicate when their name is known:

[4]:
print(lang.get('on'))
print(lang.get('clear'))
print(lang.get('a', 'b', 'c'))
on/2
clear/1
(a (object), b (object), c (object))

Notice how in the last statement we retrieve three different elements (in this case, constants) in one single call.

We can also easily inspect all constants of a certain sort:

[5]:
list(lang.get('object').domain())
[5]:
[c (object), a (object), b (object), d (object)]

We can of course inspect not only the language, but also the problem itself.

[6]:
problem
[6]:
Problem(name="BLOCKS-4-2", domain="BLOCKS")

A PDDL planning problem is made up of some initial state, some goal formula, and a number of actions. The initial state is essentially an encoding of a first-order interpretation over the language of the problem:

[7]:
problem.init
[7]:
Model[clear(a), clear(c), clear(d), handempty(), on(c,b), ontable(a), ontable(b), ontable(d)]

We can also get an extensional description of our initial state:

[8]:
print(problem.init.as_atoms())
[clear(d), clear(a), clear(c), ontable(b), ontable(d), ontable(a), on(c,b), handempty()]

Since the initial state is just another Tarski model, we can perform with it all operations that we have seen on previous tutorials, e.g. inspecting the value of some atom on it:

[9]:
clear, b = lang.get('clear', 'b')
problem.init[clear(b)]

[9]:
False

In contrast with the initial state, the goal can be any arbitrary first-order formula. Unsurprisingly, it turns out that the goal is not satisfied in the initial state;

[10]:
print(problem.goal)
print(problem.init[problem.goal])
(on(a,b) and on(b,c) and on(c,d))
False

Our blocks encoding has four actions:

[11]:
print(list(problem.actions))

['pick-up', 'put-down', 'stack', 'unstack']

We can of course retrieve and inspect each action individually:

[12]:
stack = problem.get_action('stack')
stack.parameters
[12]:
Variables(?x,?y)
[13]:
stack.precondition
[13]:
(holding(?x) and clear(?y))
[14]:
stack.effects

[14]:
[(T -> DEL(holding(?x))),
 (T -> DEL(clear(?y))),
 (T -> ADD(clear(?x))),
 (T -> ADD(handempty())),
 (T -> ADD(on(?x,?y)))]