Skip to content

Commit

Permalink
fix bug in reachability grounder and avoid the introduction of axioms…
Browse files Browse the repository at this point in the history
… from complicated goals
  • Loading branch information
roeger authored Nov 9, 2023
1 parent e6244f4 commit 85b5ffc
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 40 deletions.
11 changes: 9 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Release notes
UP Fast Downward 0.3.3
- fix bug in fast-downward-reachability grounder
- fix bug in fast-downward-reachability grounder (number of parameters)
- Transform task to avoid axioms for goals in Grounder and optimal solver:
If the goal is not a conjunction of literals, Fast Downward introduces
axioms to handle it. To avoid this, we instead introduce an artificial
goal action (removed from plans or when mapping back in grounders).
- With fast-downward-grounder, there can be several ground representatives of
the artificial goal action in the compiled task (e.g. from disjunctive
preconditions). We now give each of them an individual name.
- silence output in grounders
- support result status MEMOUT, TIMEOUT and UNSUPPORTED_PROBLEM
- support result status MEMOUT, TIMEOUT and UNSUPPORTED_PROBLEM in solvers

UP Fast Downward 0.3.2
- support UP problem kind version 2
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def run(self):
package_data={
"": ['fast_downward.py',
'fast_downward_grounder.py',
'utils.py',
'downward/fast-downward.py',
'downward/README.md', 'downward/LICENSE.md',
'downward/builds/release/bin/*',
Expand Down
60 changes: 59 additions & 1 deletion up_fast_downward/fast_downward.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import sys
import unified_planning as up
from typing import Callable, Iterator, IO, List, Optional, Tuple, Union
from unified_planning.model import ProblemKind
from unified_planning.model import ProblemKind, InstantaneousAction
from unified_planning.engines import OptimalityGuarantee
from unified_planning.engines import PlanGenerationResultStatus as ResultStatus
from unified_planning.engines import PDDLAnytimePlanner, PDDLPlanner
from unified_planning.engines import OperationMode, Credits
from unified_planning.shortcuts import BoolType, MinimizeActionCosts
from unified_planning.engines.results import LogLevel, LogMessage, PlanGenerationResult
from up_fast_downward import utils

credits = {
"name": "Fast Downward",
Expand Down Expand Up @@ -265,3 +267,59 @@ def supports(problem_kind: "ProblemKind") -> bool:
@staticmethod
def satisfies(optimality_guarantee: OptimalityGuarantee) -> bool:
return True

# To avoid the introduction of axioms with complicated goals, we introduce
# a separate goal action (later to be removed from the plan)
def _solve(
self,
problem: "up.model.AbstractProblem",
heuristic: Optional[Callable[["up.model.state.State"], Optional[float]]] = None,
timeout: Optional[float] = None,
output_stream: Optional[Union[Tuple[IO[str], IO[str]], IO[str]]] = None,
) -> "up.engines.results.PlanGenerationResult":
assert isinstance(problem, up.model.Problem)

# add a new goal atom (initially false) plus an action that has the
# original goal as precondition and sets the new goal atom
modified_problem, _, _ = utils.introduce_artificial_goal_action(problem)
return super()._solve(modified_problem, heuristic, timeout, output_stream)

# overwrite plan extraction to remove the newly introduced goal action from
# the end of the plan
def _plan_from_file(
self,
problem: "up.model.Problem",
plan_filename: str,
get_item_named: Callable[
[str],
Union[
"up.model.Type",
"up.model.Action",
"up.model.Fluent",
"up.model.Object",
"up.model.Parameter",
"up.model.Variable",
"up.model.multi_agent.Agent",
],
],
) -> "up.plans.Plan":
"""
Takes a problem, a filename and a map of renaming and returns the plan parsed from the file.
:param problem: The up.model.problem.Problem instance for which the plan is generated.
:param plan_filename: The path of the file in which the plan is written.
:param get_item_named: A function that takes a name and returns the original up.model element instance
linked to that renaming.
:return: The up.plans.Plan corresponding to the parsed plan from the file
"""
with open(plan_filename) as plan:
plan_string = plan.read()
# Remove all comments from the plan string.
plan_string = [
line
for line in plan_string.split("\n")
if not line.strip().startswith(";")
]
# Remove the last line (= goal action) from the plan string.
plan_string = plan_string[:-2]
plan_string = "\n".join(plan_string)
return self._plan_from_str(problem, plan_string, get_item_named)
164 changes: 127 additions & 37 deletions up_fast_downward/fast_downward_grounder.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from collections import defaultdict
from io import StringIO
from itertools import count
import os.path
import sys
import unified_planning as up
from functools import partial

from typing import Callable, Optional, Union
from typing import Callable, Mapping, Optional, Union, Set, Tuple
from unified_planning.model import FNode, Problem, ProblemKind, MinimizeActionCosts
from unified_planning.model.walkers import Simplifier
from unified_planning.model.action import InstantaneousAction
from unified_planning.model.operators import OperatorKind
from unified_planning.engines.compilers.utils import lift_action_instance
from unified_planning.engines.compilers.grounder import Grounder, ground_minimize_action_costs_metric
from unified_planning.engines.compilers.grounder import (
Grounder,
ground_minimize_action_costs_metric,
)
from unified_planning.engines.engine import Engine
from unified_planning.engines import Credits
from unified_planning.engines.mixins.compiler import CompilationKind
from unified_planning.engines.mixins.compiler import CompilerMixin
from unified_planning.engines.results import CompilerResult
from unified_planning.exceptions import UPUnsupportedProblemTypeError
from up_fast_downward import utils


credits = Credits(
Expand Down Expand Up @@ -104,6 +110,8 @@ def _compile(
pddl_problem = writer.get_problem().split("\n")
pddl_domain = writer.get_domain().split("\n")

# perform Fast Downward translation until (and including)
# the reachability analysis
orig_path = list(sys.path)
orig_stdout = sys.stdout
sys.stdout = StringIO()
Expand All @@ -126,6 +134,13 @@ def _compile(
prog = prolog_program(task)
model = compute_model(prog)
sys.stdout = orig_stdout
sys.path = orig_path

# The model contains an overapproximation of the reachable components
# of the task, in particular also of the reachable ground actions.
# We retreive the parameters from these actions and hand them over to
# the Grounder from the UP, which performs the instantiation on the
# side of the UP.
grounding_action_map = defaultdict(list)
exp_manager = problem.environment.expression_manager
for atom in model:
Expand All @@ -134,23 +149,17 @@ def _compile(
schematic_up_action = writer.get_item_named(action.name)
params = (
writer.get_item_named(p)
for p in atom.args[:action.num_external_parameters]
for p in atom.args[: action.num_external_parameters]
)
up_params = tuple(exp_manager.ObjectExp(p) for p in params)
grounding_action_map[schematic_up_action].append(up_params)
sys.path = orig_path

up_grounder = Grounder(grounding_actions_map=grounding_action_map)
up_res = up_grounder.compile(problem, compilation_kind)
new_problem = up_res.problem
new_problem.name = f"{self.name}_{problem.name}"

return CompilerResult(
new_problem, up_res.map_back_action_instance, self.name
)

def destroy(self):
pass
return CompilerResult(new_problem, up_res.map_back_action_instance, self.name)


class FastDownwardGrounder(Engine, CompilerMixin):
Expand Down Expand Up @@ -212,6 +221,7 @@ def resulting_problem_kind(
) -> ProblemKind:
resulting_problem_kind = problem_kind.clone()
resulting_problem_kind.unset_conditions_kind("DISJUNCTIVE_CONDITIONS")
resulting_problem_kind.unset_conditions_kind("EXISTENTIAL_CONDITIONS")
return resulting_problem_kind

def _get_fnode(
Expand All @@ -230,6 +240,7 @@ def _get_fnode(
],
],
) -> FNode:
"""Translates a Fast Downward fact back into a FNode."""
exp_manager = problem.environment.expression_manager
fluent = get_item_named(fact.predicate)
args = [problem.object(o) for o in fact.args]
Expand All @@ -254,15 +265,28 @@ def _transform_action(
"up.model.Variable",
],
],
used_action_names: Set[str],
) -> InstantaneousAction:
"""Takes a Fast Downward ground actions and builds it with the
vobabulary of the UP."""

def fnode(fact):
return self._get_fnode(fact, problem, get_item_named)

exp_manager = problem.environment.expression_manager

name_and_args = fd_action.name[1:-1].split()
name = get_item_named(name_and_args[0]).name
name_and_args[0] = name
action = InstantaneousAction("_".join(name_and_args))
full_name = "_".join(name_and_args)
if full_name in used_action_names:
for num in count():
candidate = f"{full_name}_{num}"
if candidate not in used_action_names:
full_name = candidate
break
used_action_names.add(full_name)
action = InstantaneousAction(full_name)
for fact in fd_action.precondition:
action.add_precondition(fnode(fact))
for cond, fact in fd_action.add_effects:
Expand All @@ -273,25 +297,51 @@ def fnode(fact):
action.add_effect(fnode(fact), False, c)
return action

def _compile(
self, problem: "up.model.AbstractProblem", compilation_kind: "CompilationKind"
) -> CompilerResult:
def _add_goal_action_if_complicated_goal(
self, problem: "up.model.AbstractProblem"
) -> Tuple[
"up.model.AbstractProblem",
Optional["up.model.InstantaneousAction"],
Optional[
Mapping["up.model.InstantaneousAction", "up.model.InstantaneousAction"]
],
]:
"""
Takes an instance of a :class:`~up.model.Problem` and the `GROUNDING`
:class:`~up.engines.CompilationKind` and returns a `CompilerResult`
where the problem does not have actions with parameters; so every
action is grounded.
:param problem: The instance of the `Problem` that must be grounded.
:param compilation_kind: The `CompilationKind` that must be applied on
the given problem; only `GROUNDING` is supported by this compiler
:return: The resulting `CompilerResult` data structure.
Tests whether the given problem has a complicated goal (not just
a conjunction of positive and negative fluents). If yes, it returns
a transformed problem with an artificial goal action and a single goal
fluent, where the existing actions are modified to delete the goal fluent.
The second return value is the new goal action. The third return value
maps the actions of the modified problem to the actions of the original
problem. If the goal was not complicated, it returns the original
problem, None, and None.
"""
assert isinstance(problem, Problem)
COMPLICATED_KINDS = (
OperatorKind.EXISTS,
OperatorKind.FORALL,
OperatorKind.IFF,
OperatorKind.IMPLIES,
OperatorKind.OR,
)

writer = up.io.PDDLWriter(problem)
pddl_problem = writer.get_problem().split("\n")
pddl_domain = writer.get_domain().split("\n")
def is_complicated_goal(fnode):
if fnode.node_type in COMPLICATED_KINDS or (
fnode.node_type == OperatorKind.NOT
and fnode.args[0].node_type != OperatorKind.FLUENT_EXP
):
return True
return any(is_complicated_goal(arg) for arg in fnode.args)

if not any(is_complicated_goal(g) for g in problem.goals):
# no complicated goal
return problem, None, None
else:
# To avoid the introduction of axioms with complicated goals, we
# introduce a separate goal action (later to be removed by
# map_back)
return utils.introduce_artificial_goal_action(problem, True)

def _instantiate_with_fast_downward(self, pddl_problem, pddl_domain):
orig_path = list(sys.path)
orig_stdout = sys.stdout
sys.stdout = StringIO()
Expand All @@ -312,32 +362,65 @@ def _compile(

_, _, actions, goals, axioms, _ = fd_instantiate.explore(task)
sys.stdout = orig_stdout
sys.path = orig_path
return actions, goals, axioms

def _compile(
self, problem: "up.model.AbstractProblem", compilation_kind: "CompilationKind"
) -> CompilerResult:
assert isinstance(problem, Problem)
# If necessary, perform goal transformation to avoid the introduction
# of axioms.
(
problem,
artificial_goal_action,
modified_to_orig_action,
) = self._add_goal_action_if_complicated_goal(problem)

# Ground the problem with Fast Downward
writer = up.io.PDDLWriter(problem)
pddl_problem = writer.get_problem().split("\n")
pddl_domain = writer.get_domain().split("\n")

actions, goals, axioms = self._instantiate_with_fast_downward(
pddl_problem, pddl_domain
)

if axioms:
raise UPUnsupportedProblemTypeError(axioms_msg)

# Rebuild the ground problem from Fast Downward in the UP
new_problem = problem.clone()
new_problem.name = f"{self.name}_{problem.name}"
new_problem.clear_actions()
new_problem.clear_goals()

trace_back_map = dict()

used_action_names = set()
exp_manager = problem.environment.expression_manager

# Construct Fast Downward ground actions in the UP and remember the
# mapping from the ground actions to the original actions.
for a in actions:
inst_action = self._transform_action(a, new_problem, writer.get_item_named)
inst_action = self._transform_action(
a, new_problem, writer.get_item_named, used_action_names
)
name_and_args = a.name[1:-1].split()
schematic_up_action = writer.get_item_named(name_and_args[0])
params = (writer.get_item_named(p) for p in name_and_args[1:])
up_params = tuple(exp_manager.ObjectExp(p) for p in params)
trace_back_map[inst_action] = (schematic_up_action, up_params)
schematic_up_act = writer.get_item_named(name_and_args[0])
if schematic_up_act == artificial_goal_action:
trace_back_map[inst_action] = None
else:
if modified_to_orig_action is not None:
schematic_up_act = modified_to_orig_action[schematic_up_act]
params = (writer.get_item_named(p) for p in name_and_args[1:])
up_params = tuple(exp_manager.ObjectExp(p) for p in params)
trace_back_map[inst_action] = (schematic_up_act, up_params)
new_problem.add_action(inst_action)

# Construct Fast Downward goals in the UP
for g in goals:
fnode = self._get_fnode(g, new_problem, writer.get_item_named)
new_problem.add_goal(fnode)
sys.path = orig_path

new_problem.clear_quality_metrics()
for qm in problem.quality_metrics:
Expand All @@ -347,11 +430,18 @@ def _compile(
else:
new_problem.add_quality_metric(qm)

# We need to use a more complicated function for mapping back the
# actions because "partial(lift_action_instance, map=trace_back_map)"
# cannot map an action to None (we need this for the artificial goal
# action).
mbai = lambda x: (
None
if trace_back_map[x.action] is None
else partial(lift_action_instance, map=trace_back_map)(x)
)

return CompilerResult(
new_problem,
partial(lift_action_instance, map=trace_back_map),
mbai,
self.name,
)

def destroy(self):
pass
Loading

0 comments on commit 85b5ffc

Please sign in to comment.