mirror of
https://github.com/Brandon-Rozek/website.git
synced 2024-10-30 01:12:07 -04:00
331 lines
8.2 KiB
Markdown
331 lines
8.2 KiB
Markdown
---
|
|
title: "Disjunctive Goals in PDDL"
|
|
date: 2024-04-16T11:26:28-04:00
|
|
draft: false
|
|
tags: ["Automated Planning"]
|
|
math: true
|
|
medium_enabled: false
|
|
---
|
|
|
|
Classical AI planning tries to find a sequence of actions that takes an agent from their initial state to a state that satisfies some goal.
|
|
|
|
Let's say, for example, that you want to deliver a package that's currently sitting at location `A`. The recipient said that you can drop it off at either their workplace `B` or their home `C`.
|
|
|
|
If you drop it off at a some location, then people will be nice enough to let you retrieve the package and place it back at location `A`. Graphically, we can represent it as follows:
|
|
|
|
![](/files/images/blog/disjunctive-packages.svg)
|
|
|
|
A state in classical planning is a set of atomic formulas. For our problem it will look like:
|
|
|
|
```lisp
|
|
(:init
|
|
(at-package A)
|
|
|
|
; Delivery Paths
|
|
(CONNECTED A B)
|
|
(CONNECTED A C)
|
|
(CONNECTED A D)
|
|
|
|
; Retrieval paths
|
|
(CONNECTED B A)
|
|
(CONNECTED C A)
|
|
(CONNECTED D A)
|
|
)
|
|
```
|
|
|
|
The goal in classical planning is a set of ground atomic formulas and the negations of ground atomic formulas. That means, the following isn't a "valid" goal in the classical planning model.
|
|
|
|
```lisp
|
|
(:goal (or (at-package B) (at-package C)))
|
|
```
|
|
|
|
I put valid in quotation marks since if you use the [Fast Downward](https://www.fast-downward.org/) planner, it will behind the scenes compile it to a valid planning problem. But what if you're not fortunate enough to have a planner automatically do that for you? We'll discuss three different techniques for handling disjunctive goals.
|
|
|
|
## (1) Plan for each Disjunct
|
|
|
|
A plan that delivers a package to `B` is valid for the goal of delivering it to either `B` or `C`. The same goes for delivering it to `C`. Therefore, the set of valid plans for the disjunctive goal is the union of all the plans that satisfy either of the disjuncts.
|
|
|
|
Domain File:
|
|
|
|
```lisp
|
|
(define (domain test-disjunct)
|
|
(:requirements :typing)
|
|
(:types location - object)
|
|
|
|
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location)
|
|
|
|
|
|
(:action move-package
|
|
:parameters (?l1 ?l2 - location)
|
|
:precondition (and
|
|
(at-package ?l1)
|
|
(CONNECTED ?l1 ?l2)
|
|
)
|
|
:effect (and
|
|
(at-package ?l2)
|
|
(not (at-package ?l1))
|
|
)
|
|
)
|
|
)
|
|
```
|
|
|
|
Problem File:
|
|
|
|
```lisp
|
|
(define (problem test-disjunct)
|
|
(:domain test-disjunct)
|
|
(:requirements :typing)
|
|
(:objects
|
|
A B C D - location
|
|
)
|
|
(:init
|
|
(at-package A)
|
|
(CONNECTED A B)
|
|
(CONNECTED A C)
|
|
(CONNECTED A D)
|
|
(CONNECTED B A)
|
|
(CONNECTED C A)
|
|
(CONNECTED D A)
|
|
)
|
|
(:goal (at-package B))
|
|
; (:goal (at-package C))
|
|
)
|
|
```
|
|
|
|
Call Planner:
|
|
|
|
```bash
|
|
./fast-downward.sif --alias lama-first domain-disjunct.pddl problem-disjunct.pddl
|
|
```
|
|
|
|
Result:
|
|
|
|
```
|
|
move-package a b
|
|
```
|
|
|
|
## (2) Derived Predicates (Axioms)
|
|
|
|
Axioms or derived predicates are atomic ground formula that can only appear in the precondition of actions and the goal. That is, it cannot appear in the effect.
|
|
|
|
We can create a new derived predicate `done` which when true means that we have satisfied the goal.
|
|
|
|
```lisp
|
|
(:derived (done) (or (at-package B) (at-package C)))
|
|
```
|
|
|
|
Domain File:
|
|
|
|
```lisp
|
|
(define (domain test-derived)
|
|
(:requirements :derived-predicates :typing )
|
|
(:types location - object)
|
|
|
|
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location) (done))
|
|
|
|
(:derived (done) (or (at-package B) (at-package C)))
|
|
|
|
(:action move-package
|
|
:parameters (?l1 ?l2 - location)
|
|
:precondition (and
|
|
(at-package ?l1)
|
|
(CONNECTED ?l1 ?l2)
|
|
)
|
|
:effect (and
|
|
(at-package ?l2)
|
|
(not (at-package ?l1))
|
|
|
|
)
|
|
)
|
|
)
|
|
```
|
|
|
|
Problem File:
|
|
|
|
```lisp
|
|
(define (problem test-derived)
|
|
(:domain test-derived)
|
|
(:requirements :typing)
|
|
(:objects
|
|
A B C D - location
|
|
)
|
|
(:init
|
|
(at-package A)
|
|
(CONNECTED A B)
|
|
(CONNECTED A C)
|
|
(CONNECTED A D)
|
|
(CONNECTED B A)
|
|
(CONNECTED C A)
|
|
(CONNECTED D A)
|
|
)
|
|
(:goal (done))
|
|
)
|
|
```
|
|
|
|
Call Planner:
|
|
|
|
```bash
|
|
./fast-downward.sif benchmarks/domain-derived.pddl benchmarks/problem-derived.pddl \
|
|
--search "astar(blind())"
|
|
```
|
|
|
|
Result:
|
|
|
|
```
|
|
move-package a b
|
|
```
|
|
|
|
**Note:** Not all heuristics in FastDownward support axioms. In fact this problem is not limited to FastDownward. The planner call above uses the blind heuristic which supports axioms and is safe.
|
|
|
|
Before replacing that evaluator, however, make sure to [read the docs](https://www.fast-downward.org/Doc/Evaluator) to make sure that the heuristic states that it supports axioms, and that it is safe in the presence of them.
|
|
|
|
|
|
## (3) Compilation
|
|
|
|
The core idea of this technique is to push the disjunction onto the search space. As with the last technique we create a new predicate `done`. However, instead of automatically deriving `done` through an axiom, we create an action for every disjunct whose precondition is that disjunct, and whose effect is `done`.
|
|
|
|
Example:
|
|
|
|
```lisp
|
|
(:action goal-B
|
|
:parameters ()
|
|
:precondition (at-package B)
|
|
:effect (done)
|
|
)
|
|
|
|
(:action goal-C
|
|
:parameters ()
|
|
:precondition (at-package C)
|
|
:effect (done)
|
|
)
|
|
```
|
|
|
|
Our new goal will be `(:goal (done))`. After you plan using the compiled version of the problem, drop the actions `goal-B` and `goal-C` and you have a plan for the disjunctive problem.
|
|
|
|
This technique works great when you're grabbing the first optimal plan, however, there are two considerations that need to be made when you grab more than just one plan[^1].
|
|
|
|
[^1]: The field of diverse planning looks at generating multiple plans for a given planning problem. The introduction to ["Reshaping Diverse Planning"](https://ojs.aaai.org/index.php/AAAI/article/view/6543) by Michael Katz and Shirin Sohrabi covers well the motivations behind this.
|
|
|
|
(1) There's nothing guarding against an infinite chain of `goal-B` action calls. Hence the following is a valid plan:
|
|
|
|
```
|
|
move-package a b
|
|
goal-B
|
|
goal-B
|
|
goal-B
|
|
```
|
|
|
|
We can address this by adding `(not (done))` to the preconditions of each of the goal actions.
|
|
|
|
(2) We have no effects that remove `done`, so a valid plan can move to a state that satisfies the goal and then move away.
|
|
|
|
```
|
|
move-package a b
|
|
move-package b a
|
|
```
|
|
|
|
We can address this by adding `(not (done))` to the effect of every action that is not the goal actions.
|
|
|
|
Domain File:
|
|
|
|
```lisp
|
|
(define (domain test-compile)
|
|
(:requirements :typing)
|
|
(:types location - object)
|
|
|
|
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location) (done))
|
|
|
|
(:action move-package
|
|
:parameters (?l1 ?l2 - location)
|
|
:precondition (and
|
|
(at-package ?l1)
|
|
(CONNECTED ?l1 ?l2)
|
|
)
|
|
:effect (and
|
|
(at-package ?l2)
|
|
(not (at-package ?l1))
|
|
(not (done))
|
|
)
|
|
)
|
|
|
|
(:action goal-B
|
|
:parameters ()
|
|
:precondition (and
|
|
(at-package B)
|
|
(not (done))
|
|
)
|
|
:effect (done)
|
|
)
|
|
|
|
(:action goal-C
|
|
:parameters ()
|
|
:precondition (and
|
|
(at-package C)
|
|
(not (done))
|
|
)
|
|
:effect (done)
|
|
)
|
|
)
|
|
```
|
|
|
|
Problem File:
|
|
|
|
```lisp
|
|
(define (problem test-compile)
|
|
(:domain test-compile)
|
|
(:requirements :strips)
|
|
(:objects
|
|
A B C D - location
|
|
)
|
|
(:init
|
|
(at-package A)
|
|
(CONNECTED A B)
|
|
(CONNECTED A C)
|
|
(CONNECTED A D)
|
|
(CONNECTED B A)
|
|
(CONNECTED C A)
|
|
(CONNECTED D A)
|
|
)
|
|
|
|
(:goal (done))
|
|
)
|
|
```
|
|
|
|
Planner Call:
|
|
|
|
To retrieve multiple plans, we can use the [Kstar](https://github.com/IBM/kstar) package by IBM. In particular, I had success with their [Python package](https://pypi.org/project/kstar-planner/).
|
|
|
|
```python
|
|
from kstar_planner import planners
|
|
from pathlib import Path
|
|
|
|
domain_file = Path("domain-compile.pddl")
|
|
problem_file = Path("problem-compile.pddl")
|
|
|
|
plans = planners.plan_topk(
|
|
domain_file=domain_file,
|
|
problem_file=problem_file,
|
|
number_of_plans_bound=3,
|
|
timeout=30
|
|
)
|
|
print(plans)
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
===
|
|
move-package a b
|
|
goal-b
|
|
===
|
|
move-package a c
|
|
goal-c
|
|
===
|
|
move-package a b
|
|
move-package b a
|
|
move-package a b
|
|
goal-b
|
|
```
|
|
|
|
Not sure why you would want to execute the last plan, but at least the package got to its destination at the end :D
|
|
|