From 4257bbb745f204635ad4fc16d569008abbe4c44c Mon Sep 17 00:00:00 2001 From: MarcoFavorito Date: Fri, 30 Jun 2023 17:50:45 +0200 Subject: [PATCH] feat: add PredicatesDef definition and validation --- pddl/core.py | 8 ++-- pddl/definitions/predicates_def.py | 58 ++++++++++++++++++++++++ pddl/logic/predicates.py | 70 +++++++---------------------- pddl/validation/base.py | 6 +-- pddl/validation/terms.py | 63 +++++++++++++++++++++----- scripts/whitelist.py | 71 ++++++++++++++++++------------ tests/test_domain.py | 4 +- tests/test_parser/test_domain.py | 2 +- 8 files changed, 178 insertions(+), 104 deletions(-) create mode 100644 pddl/definitions/predicates_def.py diff --git a/pddl/core.py b/pddl/core.py index 7c1dccfc..20f8816e 100644 --- a/pddl/core.py +++ b/pddl/core.py @@ -23,6 +23,7 @@ from pddl.custom_types import namelike, parse_name, to_names, to_types # noqa: F401 from pddl.definitions.base import TypesDef from pddl.definitions.constants_def import ConstantsDef +from pddl.definitions.predicates_def import _PredicatesDef from pddl.helpers.base import assert_, check, ensure, ensure_set from pddl.logic.base import And, Formula, is_literal from pddl.logic.predicates import DerivedPredicate, Predicate @@ -61,7 +62,9 @@ def __init__( self._requirements = ensure_set(requirements) self._types = TypesDef(types, self._requirements) self._constants_def = ConstantsDef(self._requirements, self._types, constants) - self._predicates = ensure_set(predicates) + self._predicates_def = _PredicatesDef( + self._requirements, self._types, predicates + ) self._derived_predicates = ensure_set(derived_predicates) self._actions = ensure_set(actions) @@ -70,7 +73,6 @@ def __init__( def _check_consistency(self) -> None: """Check consistency of a domain instance object.""" checker = TypeChecker(self._types, self.requirements) - checker.check_type(self._predicates) checker.check_type(self._actions) _check_types_in_has_terms_objects(self._actions, self._types.all_types) # type: ignore self._check_types_in_derived_predicates() @@ -102,7 +104,7 @@ def constants(self) -> AbstractSet[Constant]: @property def predicates(self) -> AbstractSet[Predicate]: """Get the predicates.""" - return self._predicates + return self._predicates_def.predicates @property def derived_predicates(self) -> AbstractSet[DerivedPredicate]: diff --git a/pddl/definitions/predicates_def.py b/pddl/definitions/predicates_def.py new file mode 100644 index 00000000..3e3a5204 --- /dev/null +++ b/pddl/definitions/predicates_def.py @@ -0,0 +1,58 @@ +# +# Copyright 2021-2023 WhiteMech +# +# ------------------------------ +# +# This file is part of pddl. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# + +"""This module implements the ConstantsDef class to handle the constants of a PDDL domain.""" +from typing import AbstractSet, Collection, Dict, Optional + +from pddl.custom_types import name as name_type +from pddl.definitions.base import TypesDef, _Definition +from pddl.exceptions import PDDLValidationError +from pddl.helpers.base import ensure_set +from pddl.logic.predicates import Predicate +from pddl.requirements import Requirements +from pddl.validation.terms import TermsValidator + + +class _PredicatesDef(_Definition): + """A set of predicates of a PDDL domain.""" + + def __init__( + self, + requirements: AbstractSet[Requirements], + types: TypesDef, + predicates: Optional[Collection[Predicate]], + ) -> None: + """Initialize the PDDL constants section validator.""" + super().__init__(requirements, types) + + self._predicates: AbstractSet[Predicate] = ensure_set(predicates) + + self._check_consistency() + + @property + def predicates(self) -> AbstractSet[Predicate]: + """Get the predicates.""" + return self._predicates + + def _check_consistency(self) -> None: + """Check consistency of the predicates definition.""" + seen_predicates_by_name: Dict[name_type, Predicate] = {} + for p in self._predicates: + # check that no two predicates have the same name + if p.name in seen_predicates_by_name: + raise PDDLValidationError( + f"these predicates have the same name: {p}, {seen_predicates_by_name[p.name]}" + ) + seen_predicates_by_name[p.name] = p + + # check that the terms of the predicate are consistent + TermsValidator(self._requirements, self._types).check_terms(p.terms) diff --git a/pddl/logic/predicates.py b/pddl/logic/predicates.py index 8b68d975..b4ffa9bb 100644 --- a/pddl/logic/predicates.py +++ b/pddl/logic/predicates.py @@ -12,34 +12,30 @@ """This class implements PDDL predicates.""" import functools -from typing import Collection, Dict, Generator, Sequence, Set, Tuple +from typing import Sequence from pddl.custom_types import name as name_type from pddl.custom_types import namelike, parse_name -from pddl.exceptions import PDDLValidationError -from pddl.helpers.base import assert_, check +from pddl.helpers.base import assert_ from pddl.helpers.cache_hash import cache_hash from pddl.logic.base import Atomic, Formula -from pddl.logic.terms import Constant, Term, _print_tag_set +from pddl.logic.terms import Constant, Term from pddl.parser.symbols import Symbols +from pddl.validation.terms import TermsValidator -class _TermsList: - """ - A class wrapper for validating sequences of terms. - - Note that this is only for internal validation of terms, and not specific to any PDDL domain type hierarchy. - """ - - def __init__(self, terms_list: Collection[Term]) -> None: - """Initialize the terms list.""" - self._terms = tuple(self.check_no_duplicate_iterator(terms_list)) +class _BaseAtomic(Atomic): + """Base class to share common code among atomic formulas classes.""" + def __init__(self, *terms: Term) -> None: + """Initialize the atomic formula.""" + TermsValidator.check_terms_consistency(terms) + self._terms = tuple(terms) self._is_ground: bool = all(isinstance(v, Constant) for v in self._terms) @property - def terms(self) -> Tuple[Term, ...]: - """Get the terms sequence.""" + def terms(self) -> Sequence[Term]: + """Get the terms.""" return self._terms @property @@ -47,60 +43,27 @@ def is_ground(self) -> bool: """Check whether the predicate is ground.""" return self._is_ground - @staticmethod - def check_no_duplicate_iterator( - terms: Collection[Term], - ) -> Generator[Term, None, None]: - """ - Iterate over terms and check that there are no duplicates. - - In particular, terms with the same name must have the same type tags. - """ - seen: Dict[name_type, Set[name_type]] = {} - for term in terms: - if term.name not in seen: - seen[term.name] = set(term.type_tags) - else: - check( - seen[term.name] == set(term.type_tags), - f"Term {term} occurred twice with different type tags: " - f"previous type tags {_print_tag_set(seen[term.name])}, " - f"new type tags {_print_tag_set(term.type_tags)}", - exception_cls=PDDLValidationError, - ) - yield term - @cache_hash @functools.total_ordering -class Predicate(Atomic): +class Predicate(_BaseAtomic): """A class for a Predicate in PDDL.""" def __init__(self, predicate_name: namelike, *terms: Term): """Initialize the predicate.""" self._name = parse_name(predicate_name) - self._terms_list = _TermsList(terms) + super().__init__(*terms) @property def name(self) -> name_type: """Get the name.""" return self._name - @property - def terms(self) -> Sequence[Term]: - """Get the terms.""" - return self._terms_list.terms - @property def arity(self) -> int: """Get the arity of the predicate.""" return len(self.terms) - @property - def is_ground(self) -> bool: - """Check whether the predicate is ground.""" - return self._terms_list.is_ground - # TODO check whether it's a good idea... # TODO allow also for keyword-based replacement # TODO allow skip replacement with None arguments. @@ -143,7 +106,7 @@ def __lt__(self, other): return super().__lt__(other) -class EqualTo(Atomic): +class EqualTo(_BaseAtomic): """Equality predicate.""" def __init__(self, left: Term, right: Term): @@ -153,11 +116,10 @@ def __init__(self, left: Term, right: Term): :param left: the left term. :param right: the right term. """ + super().__init__(left, right) self._left = left self._right = right - self._terms_list = _TermsList([self._left, self._right]) - @property def left(self) -> Term: """Get the left operand.""" diff --git a/pddl/validation/base.py b/pddl/validation/base.py index d0c21693..a24ab3da 100644 --- a/pddl/validation/base.py +++ b/pddl/validation/base.py @@ -11,7 +11,7 @@ # """Base module for validators.""" -from typing import AbstractSet, Collection +from typing import AbstractSet, Callable, Collection from pddl.custom_types import name as name_type from pddl.definitions.base import TypesDef @@ -44,10 +44,10 @@ def _check_typing_requirement(self, type_tags: Collection[name_type]) -> None: ) def _check_types_are_available( - self, type_tags: Collection[name_type], what: str + self, type_tags: Collection[name_type], what: Callable[[], str] ) -> None: """Check that the types are available in the domain.""" if not self._types.all_types.issuperset(type_tags): raise PDDLValidationError( - f"types {sorted(type_tags)} of {what} are not in available types {self._types.all_types}" + f"types {sorted(type_tags)} of {what()} are not in available types {sorted(self._types.all_types)}" ) diff --git a/pddl/validation/terms.py b/pddl/validation/terms.py index 6cb7ee18..2c366021 100644 --- a/pddl/validation/terms.py +++ b/pddl/validation/terms.py @@ -11,27 +11,66 @@ # """Module for validator of terms.""" -from typing import AbstractSet, Collection +from functools import partial +from typing import Collection, Dict, Generator, Set -from pddl.definitions.base import TypesDef -from pddl.logic.predicates import _TermsList -from pddl.logic.terms import Term -from pddl.requirements import Requirements +from pddl.custom_types import name as name_type +from pddl.exceptions import PDDLValidationError +from pddl.helpers.base import check +from pddl.logic.terms import Term, _print_tag_set from pddl.validation.base import BaseValidator class TermsValidator(BaseValidator): """Class for validator of terms.""" - def __init__( - self, requirements: AbstractSet[Requirements], types: TypesDef - ) -> None: - """Initialize the validator.""" - super().__init__(requirements, types) + @classmethod + def check_terms_consistency(cls, terms: Collection[Term]): + """ + Check that there are no duplicates. + + This is the non-iterative version of '_check_terms_consistency_iterator'. + """ + # consume the iterator + list(cls._check_terms_consistency_iterator(terms)) + + @classmethod + def _check_terms_consistency_iterator( + cls, terms: Collection[Term] + ) -> Generator[Term, None, None]: + """ + Iterate over terms and check that terms with the same name must have the same type tags. + + In particular: + - terms with the same name must be of the same term type (variable or constant); + - terms with the same name must have the same type tags. + """ + seen: Dict[name_type, Set[name_type]] = {} + for term in terms: + if term.name not in seen: + seen[term.name] = set(term.type_tags) + else: + expected_type_tags = seen[term.name] + actual_type_tags = set(term.type_tags) + check( + expected_type_tags == actual_type_tags, + f"Term {term} occurred twice with different type tags: " + f"previous type tags {_print_tag_set(expected_type_tags)}, " + f"new type tags {_print_tag_set(actual_type_tags)}", + exception_cls=PDDLValidationError, + ) + yield term def check_terms(self, terms: Collection[Term]) -> None: """Check the terms.""" - terms_iter = _TermsList.check_no_duplicate_iterator(terms) + terms_iter = self._check_terms_consistency_iterator(terms) for term in terms_iter: self._check_typing_requirement(term.type_tags) - self._check_types_are_available(term.type_tags, "terms") + self._check_types_are_available( + term.type_tags, partial(self._terms_to_string, terms) + ) + + @classmethod + def _terms_to_string(cls, terms: Collection[Term]) -> str: + """Convert terms to string for error messages.""" + return "terms ['" + "', '".join(map(str, terms)) + "']" diff --git a/scripts/whitelist.py b/scripts/whitelist.py index 13aa5f3c..c64eb607 100644 --- a/scripts/whitelist.py +++ b/scripts/whitelist.py @@ -1,33 +1,48 @@ -safe_get # unused function (pddl/helpers/base.py:95) -find # unused function (pddl/helpers/base.py:100) -ensure_formula # unused function (pddl/logic/base.py:294) +_._ # unused method (pddl/_validation.py:115) +_._ # unused method (pddl/_validation.py:121) +_._ # unused method (pddl/_validation.py:127) +_._ # unused method (pddl/_validation.py:132) +_._ # unused method (pddl/_validation.py:138) +_._ # unused method (pddl/_validation.py:144) +_._ # unused method (pddl/_validation.py:149) +_._ # unused method (pddl/_validation.py:154) +_._ # unused method (pddl/_validation.py:160) +_._ # unused method (pddl/_validation.py:165) +_._ # unused method (pddl/_validation.py:171) +_._ # unused method (pddl/_validation.py:177) +to_names # unused function (pddl/custom_types.py:82) +_.is_subtype # unused method (pddl/definitions/base.py:58) +safe_get # unused function (pddl/helpers/base.py:114) +find # unused function (pddl/helpers/base.py:119) +ensure_formula # unused function (pddl/logic/base.py:234) PEffect # unused variable (pddl/logic/effects.py:168) Effect # unused variable (pddl/logic/effects.py:170) CondEffect # unused variable (pddl/logic/effects.py:171) -_._predicates_by_name # unused attribute (pddl/parser/domain.py:51) -_.start # unused method (pddl/parser/domain.py:56) -_.domain_def # unused method (pddl/parser/domain.py:77) -_._predicates_by_name # unused attribute (pddl/parser/domain.py:110) -_.action_def # unused method (pddl/parser/domain.py:113) -_.action_parameters # unused method (pddl/parser/domain.py:131) -_.emptyor_pregd # unused method (pddl/parser/domain.py:138) -_.gd # unused method (pddl/parser/domain.py:202) -_.emptyor_effect # unused method (pddl/parser/domain.py:217) -_.c_effect # unused method (pddl/parser/domain.py:232) -_.p_effect # unused method (pddl/parser/domain.py:247) -_.cond_effect # unused method (pddl/parser/domain.py:254) -_.atomic_formula_term # unused method (pddl/parser/domain.py:262) -_.atomic_formula_skeleton # unused method (pddl/parser/domain.py:293) -_.typed_list_variable # unused method (pddl/parser/domain.py:312) -_.type_def # unused method (pddl/parser/domain.py:329) -_.start # unused method (pddl/parser/problem.py:39) -_.problem_def # unused method (pddl/parser/problem.py:52) -_.problem_domain # unused method (pddl/parser/problem.py:56) -_.problem_requirements # unused method (pddl/parser/problem.py:60) -_.domain__type_def # unused method (pddl/parser/problem.py:83) -_.literal_name # unused method (pddl/parser/problem.py:92) -_.gd_name # unused method (pddl/parser/problem.py:105) -_.atomic_formula_name # unused method (pddl/parser/problem.py:116) +_.is_ground # unused property (pddl/logic/predicates.py:41) +_._predicates_by_name # unused attribute (pddl/parser/domain.py:42) +_.start # unused method (pddl/parser/domain.py:47) +_.domain_def # unused method (pddl/parser/domain.py:68) +_._predicates_by_name # unused attribute (pddl/parser/domain.py:101) +_.action_def # unused method (pddl/parser/domain.py:104) +_.action_parameters # unused method (pddl/parser/domain.py:122) +_.emptyor_pregd # unused method (pddl/parser/domain.py:129) +_.gd # unused method (pddl/parser/domain.py:193) +_.emptyor_effect # unused method (pddl/parser/domain.py:208) +_.c_effect # unused method (pddl/parser/domain.py:223) +_.p_effect # unused method (pddl/parser/domain.py:238) +_.cond_effect # unused method (pddl/parser/domain.py:245) +_.atomic_formula_term # unused method (pddl/parser/domain.py:253) +_.atomic_formula_skeleton # unused method (pddl/parser/domain.py:284) +_.typed_list_variable # unused method (pddl/parser/domain.py:299) +_.type_def # unused method (pddl/parser/domain.py:322) +_.start # unused method (pddl/parser/problem.py:40) +_.problem_def # unused method (pddl/parser/problem.py:53) +_.problem_domain # unused method (pddl/parser/problem.py:57) +_.problem_requirements # unused method (pddl/parser/problem.py:61) +_.domain__type_def # unused method (pddl/parser/problem.py:84) +_.literal_name # unused method (pddl/parser/problem.py:93) +_.gd_name # unused method (pddl/parser/problem.py:106) +_.atomic_formula_name # unused method (pddl/parser/problem.py:117) OpSymbol # unused variable (pddl/parser/symbols.py:17) OpRequirement # unused variable (pddl/parser/symbols.py:18) ROUND_BRACKET_LEFT # unused variable (pddl/parser/symbols.py:24) @@ -48,5 +63,3 @@ REQUIREMENTS # unused variable (pddl/parser/symbols.py:51) TYPES # unused variable (pddl/parser/symbols.py:52) ALL_REQUIREMENTS # unused variable (pddl/parser/symbols.py:80) -to_names # unused function (pddl/custom_types.py:79) -_.is_subtype # unused method (pddl/definitions/types_def.py:52) diff --git a/tests/test_domain.py b/tests/test_domain.py index d9840076..70ca01d0 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -118,7 +118,7 @@ def test_constants_type_not_available() -> None: with pytest.raises( PDDLValidationError, - match=rf"types \['t1'\] of terms are not in available types {{'{my_type}'}}", + match=r"types \['t1'\] of terms \['a'\] are not in available types \['my_type'\]", ): Domain("test", requirements={Requirements.TYPING}, constants={a}, types=type_set) # type: ignore @@ -147,7 +147,7 @@ def test_predicate_variable_type_not_available() -> None: with pytest.raises( PDDLValidationError, - match=rf"types \['t1', 't2'\] of term {re.escape(repr(x))} are not in available types {{'{my_type}'}}", + match=r"types \['t1', 't2'\] of terms \['\?a'\] are not in available types \['my_type'\]", ): Domain("test", requirements={Requirements.TYPING}, predicates={p}, types=type_set) # type: ignore diff --git a/tests/test_parser/test_domain.py b/tests/test_parser/test_domain.py index 5019456b..40132d1a 100644 --- a/tests/test_parser/test_domain.py +++ b/tests/test_parser/test_domain.py @@ -247,7 +247,7 @@ def test_variables_typed_with_not_available_types() -> None: with pytest.raises( lark.exceptions.VisitError, - match=r"types \['t2'\] of term Variable\(x\) are not in available types \{'t1'\}", + match=r"types \['t2'\] of terms \['\?x'\] are not in available types \['t1'\]", ): DomainParser()(domain_str)