mrlab-ai/PlanForge

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

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.

What each construct compiles to. Every row has a fixture that fails if the behaviour is lost.
ConstructHow 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.

Every row was checked by running the translator on a task that uses the construct, except :duration-inequalities, :continuous-effects and :goal-utilities. For those three there is no handling anywhere in the code, and the row was established by reading it.
FlagStatusDetail
:stripsSupportedThe base language.
:typingSupportedType hierarchies included. Types are compiled into predicates before grounding. (either a b) is not read and fails on the type token.
:negative-preconditionsSupported(not (p ?x)) in preconditions, goals and effect conditions. Only around a single atom or a comparison. (not (and ...)) and (not (or ...)) fail.
:action-costsSupported(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, :fluentsSupportedThe main extension. See conditions and effects.
:derived-predicatesSupported(:derived ...), layered and recursive. exists works inside a body. Several blocks for one head act as a disjunction. Nine fixtures cover this.
:equalityPartial(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-preconditionsSupportedAn 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-effectsPartialwhen guarding a single propositional or numeric effect works. A conjunctive consequent, (when C (and A B)), fails.
:universal-preconditions, :existential-preconditions, :quantified-preconditionsMishandledNot implemented. See the six cases.
:adlSupportedQuantified preconditions, universal effects, disjunction and imply are all handled, wherever they appear. See ADL.
:object-fluentsRejectedA 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-literalsNot supportedNothing 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, :constraintsNot supportedNo 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-utilitiesNot supportedNo 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

SectionHandling
:requirementsParsed for its shape and then dropped. See requirement levels.
:typesRead, including hierarchies. An untyped parameter defaults to object.
:constantsRead.
:predicatesRead.
:functionsRead. These are the numeric fluents.
:actionRead: :parameters, :precondition and :effect.
:derivedRead. Derived predicates become axioms, arranged into layers.
:global-constraintRead, as an axiom. A constraint that must hold in every state.
:axiomRejected 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 elseSkipped, with a warning that names the section.

Conditions and goals

The same rules apply in preconditions, goals, axiom bodies and effect conditions.

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

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.

SectionCarries
versionThe format version, 4.
metricThe direction, and the numeric variable being optimised.
variableOne 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_groupMutex groups found by the invariant analysis, when it finds any. The blocks-4-0 fixture yields five.
stateThe initial value of every finite-domain variable.
goalThe goal, as variable and value pairs.
operatorOne per grounded action: name, prevail conditions, effects with their own preconditions and conditions, and the cost.
ruleOne per grounded propositional axiom. This is how a :derived predicate arrives.
numeric_variablesThe numeric variables and their kind: regular, constant, cost or derived.
numeric_stateTheir initial values.
comparison_axiomsOne per numeric comparison. This is what gives a comparison its own derived variable.
numeric_axiomsAssignment axioms. The arithmetic is flattened to one operator per node.
global_constraintThe 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.

Paths are relative to tests/assets/.
FixtureWhat it exercisesCheapest cost
strips-pddl-files/blocks-4-0A purely propositional task, with no numbers at all6
numeric-pddl-files/deliveryNumeric fluents, two vehicles22
numeric-pddl-files/sailingFluents with continuous values23
numeric-conditions/conditional-numeric-effectwhen guarding a numeric effect3
numeric-conditions/strict-comparisonA strict inequality as a precondition2
derived-predicates/recursive-closureA recursive :derived predicate3
derived-predicates/layered-chainAxioms spread over several layers4

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

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.