Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove mapping, proposition names *are* PDDL ground fluents. Fix ambiguity in DP names #262

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ from pddl.parser.problem import ProblemParser
from pylogics.parsers import parse_pltl
from plan4past.compiler import Compiler

formula = "on_b_a & O(ontable_c)"
formula = '"on b a" & O("ontable c")'
domain_parser = DomainParser()
problem_parser = ProblemParser()

Expand Down
22 changes: 4 additions & 18 deletions plan4past/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from pylogics.syntax.base import Formula

from plan4past.compiler import Compiler
from plan4past.utils.mapping_parser import mapping_parser

DEFAULT_NEW_DOMAIN_FILENAME: str = "new-domain.pddl"
DEFAULT_NEW_PROBLEM_FILENAME: str = "new-problem.pddl"
Expand Down Expand Up @@ -62,13 +61,6 @@
help="The path to the PPLTL goal formula.",
type=click.Path(exists=True, readable=True),
)
@click.option(
"-m",
"--mapping",
help="The mapping file.",
type=click.Path(exists=True, readable=True),
default=None,
)
@click.option(
"-od",
"--out-domain",
Expand All @@ -83,20 +75,14 @@
help="Path to PDDL file to store the new problem.",
type=click.Path(dir_okay=False),
)
def cli(domain, problem, goal_inline, goal_file, mapping, out_domain, out_problem):
def cli(domain, problem, goal_inline, goal_file, out_domain, out_problem):
"""Plan4Past: Planning for Pure-Past Temporally Extended Goals."""
goal = _get_goal(goal_inline, goal_file)

in_domain, in_problem, formula = _parse_instance(domain, problem, goal)

var_map = (
mapping_parser(Path(mapping).read_text(encoding="utf-8"), formula)
if mapping
else None
)

compiled_domain, compiled_problem = _compile_instance(
in_domain, in_problem, formula, var_map
in_domain, in_problem, formula
)

try:
Expand Down Expand Up @@ -137,9 +123,9 @@ def _parse_instance(in_domain, in_problem, goal) -> Tuple[Domain, Problem, Formu
return domain, problem, formula


def _compile_instance(domain, problem, formula, mapping) -> Tuple[Domain, Problem]:
def _compile_instance(domain, problem, formula) -> Tuple[Domain, Problem]:
"""Compile the PDDL domain and problem files and the PPLTL goal formula."""
compiler = Compiler(domain, problem, formula, mapping)
compiler = Compiler(domain, problem, formula)
compiler.compile()
compiled_domain, compiled_problem = compiler.result

Expand Down
43 changes: 5 additions & 38 deletions plan4past/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@
#

"""Compiler from PDDL Domain and PPLTL into a new PDDL domain."""
from typing import AbstractSet, Dict, Optional, Set, Tuple
from typing import AbstractSet, Optional, Set, Tuple

from pddl.core import Action, Domain, Problem, Requirements
from pddl.logic import Constant
from pddl.logic.base import And, Not
from pddl.logic.effects import AndEffect, When
from pddl.logic.predicates import DerivedPredicate, Predicate
Expand All @@ -35,7 +34,6 @@
from plan4past.helpers.utils import (
add_val_prefix,
check_,
default_mapping,
remove_before_prefix,
replace_symbols,
)
Expand All @@ -54,24 +52,17 @@ def __init__(
domain: Domain,
problem: Problem,
formula: Formula,
from_atoms_to_fluent: Optional[Dict[PLTLAtomic, Predicate]] = None,
) -> None:
"""
Initialize the compiler.

:param domain: the domain
:param problem: the problem
:param formula: the formula
:param from_atoms_to_fluent: optional mapping from atoms to fluent
"""
self.domain = domain
self.problem = problem
self.formula = rewrite(formula)
if from_atoms_to_fluent:
self.from_atoms_to_fluent = from_atoms_to_fluent
self.validate_mapping(domain, formula, from_atoms_to_fluent)
else:
self.from_atoms_to_fluent = default_mapping(self.formula)

check_(self.formula.logic == Logic.PLTL, "only PPLTL is supported!")

Expand All @@ -81,29 +72,6 @@ def __init__(

self._derived_predicates: Set[DerivedPredicate] = set()

@classmethod
def validate_mapping(
cls,
_domain: Domain,
_formula: Formula,
from_atoms_to_fluent: Dict[PLTLAtomic, Predicate],
):
"""
Check that the mapping is valid wrt the problem instance.

In particular:
- check that all the formula atoms are covered (TODO)
- check that all the atoms are legal wrt the formula
- check that the fluents are legal wrt the domain

:param _domain:
:param _formula:
:param from_atoms_to_fluent:
:return:
"""
for _atom, fluent in from_atoms_to_fluent.items():
check_(all(isinstance(t, Constant) for t in fluent.terms))

@property
def result(self) -> Tuple[Domain, Problem]:
"""Get the result."""
Expand All @@ -121,9 +89,7 @@ def compile(self):
def _compile_domain(self):
"""Compute the new domain."""
new_predicates = predicates(self.formula).union(val_predicates(self.formula))
new_derived_predicates = derived_predicates(
self.formula, self.from_atoms_to_fluent
)
new_derived_predicates = derived_predicates(self.formula)
new_whens = _compute_whens(self.formula)
domain_actions = _update_domain_actions_det(self.domain.actions, new_whens)

Expand Down Expand Up @@ -167,12 +133,13 @@ def _compile_problem(self):

def _compute_whens(formula: Formula) -> Set[When]:
"""Compute conditional effects for formula progression."""
formula_predicates = predicates(formula)
return {
When(Predicate(add_val_prefix(remove_before_prefix(p.name))), p)
for p in predicates(formula)
for p in formula_predicates
}.union(
When(Not(Predicate(add_val_prefix(remove_before_prefix(p.name)))), Not(p))
for p in predicates(formula)
for p in formula_predicates
)


Expand Down
132 changes: 100 additions & 32 deletions plan4past/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,64 +21,63 @@
#

"""Miscellanea utilities."""
from typing import Dict
import re
from typing import Collection, Type

from pddl.logic import Predicate, constants
from pylogics.syntax.base import Formula
from pylogics.syntax.pltl import Atomic as PLTLAtomic

from plan4past.utils.atoms_visitor import find_atoms
_PDDL_NAME_REGEX = "[A-Za-z][-_A-Za-z0-9]*"
_GROUND_FLUENT_REGEX = re.compile(
rf"(\"({_PDDL_NAME_REGEX})( {_PDDL_NAME_REGEX})*\")|({_PDDL_NAME_REGEX})"
Copy link
Member Author

@marcofavorito marcofavorito Jul 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either

"predicate_name const1 const2 ..." or predicate_name.

E.g.:

O("on a b") & O(emptyhand)

)
VAL_PREFIX = "VAL__"
_LEFT_PAR = "LPAR__"
_RIGHT_PAR = "__RPAR"
Y_PREFIX = "Y__"
_NOT = "NOT__"
_AND = "__AND__"
_OR = "__OR__"
_QUOTE = "__QUOTE__"


def add_val_prefix(name: str):
"""Add the 'prime' prefix."""
return "val-" + name.replace('"', "")
return VAL_PREFIX + name.replace('"', _QUOTE)


def remove_before_prefix(name: str):
"""Remove the 'Y' prefix."""
return (
name.replace("Y-", "")
if name.startswith("Y-")
else name.replace("Y", "", 1)
if name.startswith("Y")
name.replace(Y_PREFIX, "")
if name.startswith(Y_PREFIX)
else re.sub(f"{_RIGHT_PAR}$", "", name.replace("Y" + _LEFT_PAR, "", 1))
if name.startswith("Y" + _LEFT_PAR)
else name
)


def remove_val_prefix(name: str):
"""Remove the 'prime' prefix."""
return name.replace("val-", "") if name.startswith("val-") else name
return name.replace(VAL_PREFIX, "") if name.startswith(VAL_PREFIX) else name


def replace_symbols(name: str):
"""Stringify symbols."""
return (
name.replace('"', "")
.replace("(", "")
.replace(")", "")
.replace("&", "and")
.replace("|", "or")
.replace("~", "not-")
.replace("!", "not-")
name.replace('"', _QUOTE)
.replace("(", _LEFT_PAR)
.replace(")", _RIGHT_PAR)
.replace("&", _AND)
.replace("|", _OR)
.replace("~", _NOT)
.replace("!", _NOT)
.replace(" ", "-")
)


def default_mapping(formula: Formula) -> Dict[PLTLAtomic, Predicate]:
"""Compute mapping from atoms to fluents using underscores."""
symbols = find_atoms(formula)
from_atoms_to_fluents = {}
for symbol in symbols:
name, *cons = symbol.name.split("_")
if cons:
from_atoms_to_fluents[symbol] = Predicate(name, *constants(" ".join(cons)))
else:
from_atoms_to_fluents[symbol] = Predicate(name)
return from_atoms_to_fluents


def check_(condition: bool, message: str = "") -> None:
def check_(
condition: bool, message: str = "", exception_cls: Type[Exception] = AssertionError
) -> None:
"""
User-defined assert.

Expand All @@ -87,4 +86,73 @@ def check_(condition: bool, message: str = "") -> None:
https://bandit.readthedocs.io/en/1.7.5/plugins/b101_assert_used.html
"""
if not condition:
raise AssertionError(message)
raise exception_cls(message)


def parse_ground_fluent(symbol: str) -> Predicate:
"""
Parse a ground fluent.

:param symbol: the ground fluent
:return: the predicate
"""
match = _GROUND_FLUENT_REGEX.fullmatch(symbol)
if match is None:
raise ValueError(f"invalid PDDL symbol in formula: {symbol}")

if '"' in symbol:
tokens = symbol[1:-1].split(" ", 1)
predicate_name, cons = (
(tokens[0], tokens[1]) if len(tokens) > 1 else (tokens[0], "")
)
return Predicate(predicate_name, *constants(cons))
return Predicate(symbol)


def _check_does_not_start_with(symbol: str, prefixes: Collection[str]) -> None:
"""
Check if a symbol does not start with a given prefix.

:param symbol: the symbol
:param prefixes: the prefixes to check
"""
for prefix in prefixes:
check_(
not symbol.startswith(prefix),
f"invalid symbol: symbol '{symbol}' cannot start with {prefix}",
exception_cls=ValueError,
)


def _check_not_in(symbol: str, forbidden_substrings: Collection[str]) -> None:
"""
Check if a string is not in a set of symbols.

:param symbol: the symbol
:param forbidden_substrings: the set of forbidden substrings
"""
for s in forbidden_substrings:
check_(
s not in symbol,
f"invalid symbol: symbol '{symbol}' contains {s}",
exception_cls=ValueError,
)


def validate(symbol: str) -> None:
"""
Validate a symbol.

:param symbol: the symbol
"""
# remove the double quotes
symbol_unquoted = symbol.replace('"', "")

# check if the symbol does not start with the 'val__' or the 'Y__' prefix
_check_does_not_start_with(symbol_unquoted, [VAL_PREFIX, Y_PREFIX])

# check if the symbol does not contain forbidden substrings
_check_not_in(symbol_unquoted, [_LEFT_PAR, _RIGHT_PAR, _AND, _OR, _NOT, _QUOTE])

# check if the symbol is a valid PDDL ground fluent
parse_ground_fluent(symbol)
Loading