PDDL support
PlanForge reads PDDL 2.1 level 1, which is the propositional and numeric language without time, plus the derived predicates of PDDL 2.2.
This page says what the translator accepts, construct by construct. It then says where the limits are.
The accepted fragment is ADL plus numeric fluents plus axioms. A domain declaring a requirement the translator does not implement is refused by name rather than translated as though the construct were absent.
Contents
- ADL
- Requirement levels
- Input and translation
- Domain sections
- Conditions and goals
- Effects
- What the translator writes
- Fixtures you can check this against
- Limits
- Benchmarks
ADL
ADL is supported. Quantified preconditions, universal effects, disjunction
and imply are all handled, wherever they appear: in an action
precondition, in the condition of a conditional effect, in a
:derived body, and in the goal.
Six of these constructs used to be accepted by the parser and then dropped or
read wrongly, four of them returning a plan for a task whose real optimum was
higher. That was issue
46 and it is fixed. Each of the six now has a fixture under
tests/assets/adl, and each fixture is built so that losing the
behaviour changes the optimal cost rather than only making the task unsolvable.
| Construct | How it is handled |
|---|---|
forall in a precondition, a goal or an effect condition |
Replaced by the negation of a new derived predicate, which derives the dual existential. Grounding can look for one witness but cannot check a property of every object at once, so the quantifier is expressed through its dual. |
forall in an effect |
Grounded into one effect per object of the quantified type. |
exists anywhere |
Its variables move into the parameter list of the action or axiom that holds it. They do not become part of its name. |
or in a precondition |
Pulled to the root, then the action is split into one action per disjunct. |
or in a :derived body |
Split into several axioms with the same head. Several bodies proving one
head already act as a disjunction, so the axiom layer needs no notion of
or. |
| A disjunctive or otherwise complicated goal | Compiled into an axiom, whose body is then split like any other. The goal becomes the single derived atom that axiom proves. |
imply |
(imply a b) becomes (or (not a) b), with the
antecedent negated into negation normal form however compound it is. |
:requirements is read. A domain declaring a requirement the
translator does not implement, :durative-actions for instance, is
refused with a message naming it, rather than accepted and then translated as
though the construct were absent.
Requirement levels
The table states the accepted fragment as :requirements flags, so
that you can check it against your own domain. The planner reads these flags and
refuses a domain declaring one it does not implement, so the table now describes
something the planner enforces rather than only a property of the code.
| Flag | Status | Detail |
|---|---|---|
:strips | Supported | The base language. |
:typing | Supported | Type hierarchies included. Types are compiled into predicates before grounding. (either a b) is not read and fails on the type token. |
:negative-preconditions | Supported | (not (p ?x)) in preconditions, goals and effect conditions. Only around a single atom or a comparison. (not (and ...)) and (not (or ...)) fail. |
:action-costs | Supported | (increase (total-cost) ...) at the top level of an effect. A cost must be a non-negative integer. A fractional cost is rejected. |
:numeric-fluents, :fluents | Supported | The main extension. See conditions and effects. |
:derived-predicates | Supported | (:derived ...), layered and recursive. exists works inside a body. Several blocks for one head act as a disjunction. Nine fixtures cover this. |
:equality | Partial | (not (= ?x ?y)) works, because every object is made equal to itself in the initial state, which makes inequality statically decidable. A positive (= ?x ?y) between two objects is read as a comparison between two zero-argument fluents and removes the action. |
:disjunctive-preconditions | Supported | An or is pulled to the root of the condition and then split: an action becomes one action per disjunct, an axiom becomes several axioms with the same head. See ADL. |
:conditional-effects | Partial | when guarding a single propositional or numeric effect works. A conjunctive consequent, (when C (and A B)), fails. |
:universal-preconditions, :existential-preconditions, :quantified-preconditions | Mishandled | Not implemented. See the six cases. |
:adl | Supported | Quantified preconditions, universal effects, disjunction and imply are all handled, wherever they appear. See ADL. |
:object-fluents | Rejected | A typed function return is parsed and discarded. An object-valued entry in :init is then an explicit error: numeric init fact for PNE loc-of(t1) must assign a numeric constant, got PNE p1(). |
:durative-actions, :duration-inequalities, :continuous-effects, :timed-initial-literals | Not supported | Nothing temporal. A (:durative-action ...) block is treated as an unknown section and skipped with a warning, which leaves the domain with no actions. Translation then fails on the first unreachable goal atom. A timed initial literal fails on its nested literal. |
:preferences, :constraints | Not supported | No PDDL 3. A (preference ...) in a goal fails. A problem-level (:constraints ...) block is dropped with no message at all, so the plan ignores it silently. |
:goal-utilities | Not supported | No soft goals and no utilities. |
Input and translation
Two positional arguments are a domain file and a problem file. One positional argument is a SAS+ file that has already been translated.
planforge --search 'astar(lmcutnumeric())' domain.pddl problem.pddl
planforge --search 'astar(lmcutnumeric())' task.sas
On the PDDL route, translation and grounding happen in the same process as the search. No SAS+ text is produced along the way.
When you want the text itself, to hand the task to another planner or to attach it to a bug report, use the second program. It stops after translation.
planforge-translator translate domain.pddl problem.pddl --output task.sas
Without --output it writes output.sas.
Its one other option is --layer-strategy, which decides how
derived variables are spread over axiom layers. min is the default
and puts as many variables as possible into one layer. max gives
every variable its own layer unless it sits in a cycle. Any other value is
rejected while the command line is parsed.
$ planforge-translator translate --layer-strategy nope domain.pddl problem.pddl
error: invalid value 'nope' for '--layer-strategy <LAYER_STRATEGY>': unknown layer strategy "nope"; use `min` or `max`
Both routes reach the same task. Translating the delivery fixture and then searching the resulting file reports the same 49 finite-domain variables and 12 numeric variables, expands the same 73 states, and returns the same plan of cost 22 as the single call does.
Domain sections
| Section | Handling |
|---|---|
:requirements | Parsed for its shape and then dropped. See requirement levels. |
:types | Read, including hierarchies. An untyped parameter defaults to object. |
:constants | Read. |
:predicates | Read. |
:functions | Read. These are the numeric fluents. |
:action | Read: :parameters, :precondition and :effect. |
:derived | Read. Derived predicates become axioms, arranged into layers. |
:global-constraint | Read, as an axiom. A constraint that must hold in every state. |
:axiom | Rejected on purpose. PDDL 2.1's (:axiom :vars ... :context ... :implies ...) is a different block shape, not another spelling of :derived. Parsing it as one would mistake :vars for the head predicate, so the error tells you to write (:derived (NAME ?x - t) CONDITION) instead. |
| anything else | Skipped, with a warning that names the section. |
Conditions and goals
The same rules apply in preconditions, goals, axiom bodies and effect conditions.
- Atoms and negated atoms.
notaround a single atom or a comparison.notaround a compound expression is not read and fails on the inner list. and, everywhere.or, in an action precondition only. There it is compiled away by splitting the action into one copy per disjunct. Elsewhere it is treated as true.exists, in a:derivedbody only. The bound variables become parameters of the axiom. In an action precondition it removes the action.- Numeric comparisons
<,<=,=,>=and>, each over exactly two operands. Each can also appear insidenot. imply, with a single-literal antecedent. A compound antecedent is not negated.forallparses and is then treated as true. Do not use it.
A comparison becomes a variable of its own in the SAS+ task. That is what lets the abstraction refinement loop split on the operands of a comparison, and it is why numeric heuristics can say anything useful about a numeric condition.
= is read as a numeric comparison whenever it is not directly
inside a not. So it works between fluent expressions, and
(not (= ?x ?y)) works as object inequality. A bare positive
(= ?x ?y) between two objects is read as a comparison between two
zero-argument fluents, and it removes the action.
Effects
- Conjunctions of effects, and negated atoms as delete effects.
- Conditional effects.
whenguarding one propositional or numeric effect. The consequent must be a single effect. A conjunctive consequent,(when C (and A B)), is not read and fails on the inner list. Write onewhenper effect instead. - Numeric assignment. All five PDDL operators:
assign,increase,decrease,scale-upandscale-down. The target must be a function applied to arguments, not a compound expression. - Universal effects are not supported. A
foralleffect parses and is then dropped.
Numeric expressions
Inside a comparison or an assignment: numeric constants, function
applications, the operators +, -, * and
/, and unary - as additive inverse. The operators take
two or more operands.
The fluent total-cost is recognised as the integer accumulator
for action costs.
What the translator writes
The output is Fast Downward's SAS+ text format, declared as version 4. That is the familiar layout plus the sections the numeric extension needs.
These are all of the sections, and every one of them appears in real output from the fixtures in this repository.
| Section | Carries |
|---|---|
version | The format version, 4. |
metric | The direction, and the numeric variable being optimised. |
variable | One per finite-domain variable: name, axiom layer, domain size and one name per value. The axiom layer is -1 when no axiom derives the variable. |
mutex_group | Mutex groups found by the invariant analysis, when it finds any. The blocks-4-0 fixture yields five. |
state | The initial value of every finite-domain variable. |
goal | The goal, as variable and value pairs. |
operator | One per grounded action: name, prevail conditions, effects with their own preconditions and conditions, and the cost. |
rule | One per grounded propositional axiom. This is how a :derived predicate arrives. |
numeric_variables | The numeric variables and their kind: regular, constant, cost or derived. |
numeric_state | Their initial values. |
comparison_axioms | One per numeric comparison. This is what gives a comparison its own derived variable. |
numeric_axioms | Assignment axioms. The arithmetic is flattened to one operator per node. |
global_constraint | The constraint that must hold in every state. |
The delivery fixture comes out as 49 finite-domain variables, 12 numeric variables, 136 operators and one axiom rule.
The reader parses the declared version but does not check it. A file declaring version 3 is read without complaint. So what has to match is the section layout, not the number.
Fixtures you can check this against
The repository ships small PDDL tasks under tests/assets. The
test suite pins the cheapest plan cost of each one, so they double as worked
examples of the constructs above.
Every row below is a run of --search 'astar(lmcutnumeric())' on a
default build.
| Fixture | What it exercises | Cheapest cost |
|---|---|---|
strips-pddl-files/blocks-4-0 | A purely propositional task, with no numbers at all | 6 |
numeric-pddl-files/delivery | Numeric fluents, two vehicles | 22 |
numeric-pddl-files/sailing | Fluents with continuous values | 23 |
numeric-conditions/conditional-numeric-effect | when guarding a numeric effect | 3 |
numeric-conditions/strict-comparison | A strict inequality as a precondition | 2 |
derived-predicates/recursive-closure | A recursive :derived predicate | 3 |
derived-predicates/layered-chain | Axioms spread over several layers | 4 |
Limits
Abstractions need a conjunctive goal
An abstract operator is derived from a task operator, and no task operator
writes a derived variable. Only the axioms do. So a goal on a
:derived predicate is a goal an abstraction has nothing to reach
for.
Rather than let each abstraction family invent its own reading of such a goal, all of them refuse the task and name the variable and its axiom layer.
Error: Custom { kind: Other, error: "failed to build domain abstraction: CEGAR failed to build abstraction: abstractions support conjunctive goals only: goal fact Fact(var: 4, value: 0) names the derived variable \"var4\" (\"\") in axiom layer 1, which no operator writes" }
This covers a :derived predicate in the goal. It also covers the
goal-reachability predicate the translator introduces for a disjunctive,
quantified or nested goal.
Numeric comparisons are not affected. A comparison variable is derived in the SAS+ sense too, but refining the operands of a comparison is exactly what the numeric refinement loop does.
Nothing else is restricted this way. The same fixture that the domain
abstraction refuses solves normally under astar(lmcutnumeric()),
because a non-abstraction heuristic tests the goal in a state the axiom evaluator
has already closed.
A plan metric must name a single fluent
(:metric minimize (total-cost)) is used as written.
A compound metric expression is not supported, and it is not rejected either.
It is replaced by total-cost with no error. So if your task
minimises an arithmetic combination of fluents, PlanForge is not optimising what
you wrote.
Other limits
- Pattern databases and some Cartesian abstractions need a restricted
task. Pass
--restrict-task, or supply a task that is already restricted. Asking for one of those heuristics without it is an error that names the offending comparison axiom, not a silent conversion. - Comparisons are binary. A comparison with more or fewer than two operands is rejected.
- An assignment target is primitive. The left-hand side of an assignment effect must be a function application, not an expression.
- Sequential plans only. No durative actions, no timed initial literals, no concurrency, no preferences and no soft goals.
- No preferred operators. The FF heuristic returns an estimate and no preferred-operator set, so greedy search cannot use one.
- Grounding is eager. The whole task is grounded before the search starts. An instance whose grounding does not fit in memory is out of reach however easy the search would have been.
- A trivially unsolvable task crashes instead of being
reported. If a goal atom is neither reachable nor statically
decided, translation stops with
condition atom ... has no SAS variable; it is neither reachable nor statically decided. No plan exists, so the outcome is correct, but it arrives as a panic and exit code 101 rather than as a verdict. - Most rejections are panics rather than errors. Only the S-expression reader and a handful of translation checks return a typed error. A construct outside the fragment usually aborts with a Rust panic message. It names what it choked on, but it is not a tidy diagnostic, and it is easy to mistake for a bug in your PDDL when it is a limit of the translator.
Benchmarks
No benchmark set is shipped with PlanForge or from this site. The PDDL tasks in the repository are small regression fixtures with pinned costs, not a benchmark suite.
For classical benchmarks, use Fast Downward's own collection at github.com/aibasel/downward-benchmarks. It holds the IPC suites that Fast Downward is evaluated on, one directory per domain. The STRIPS domains there run in PlanForge. The ADL domains do not, for the reasons on this page.
That collection is propositional. It has no numeric domains, so it does not exercise the part of PlanForge that is new. For numeric domains, go to the numeric tracks of the International Planning Competition.