Skip to content

Commit

Permalink
feat: add ConstantsDef definition and validation
Browse files Browse the repository at this point in the history
  • Loading branch information
marcofavorito committed Jun 30, 2023
1 parent 24a98cc commit 3fb63de
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 21 deletions.
6 changes: 3 additions & 3 deletions pddl/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pddl.custom_types import name as name_type
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.helpers.base import assert_, check, ensure, ensure_set
from pddl.logic.base import Formula, TrueFormula, is_literal
from pddl.logic.predicates import DerivedPredicate, Predicate
Expand Down Expand Up @@ -59,7 +60,7 @@ def __init__(
self._name = parse_name(name)
self._requirements = ensure_set(requirements)
self._types = TypesDef(types, self._requirements)
self._constants = ensure_set(constants)
self._constants_def = ConstantsDef(self._requirements, self._types, constants)
self._predicates = ensure_set(predicates)
self._derived_predicates = ensure_set(derived_predicates)
self._actions = ensure_set(actions)
Expand All @@ -69,7 +70,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._constants)
checker.check_type(self._predicates)
checker.check_type(self._actions)
_check_types_in_has_terms_objects(self._actions, self._types.all_types) # type: ignore
Expand Down Expand Up @@ -97,7 +97,7 @@ def requirements(self) -> AbstractSet["Requirements"]:
@property
def constants(self) -> AbstractSet[Constant]:
"""Get the constants."""
return self._constants
return self._constants_def.constants

@property
def predicates(self) -> AbstractSet[Predicate]:
Expand Down
41 changes: 41 additions & 0 deletions pddl/definitions/constants_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# 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, Optional

from pddl.definitions.base import TypesDef, _Definition
from pddl.helpers.base import ensure_set
from pddl.logic import Constant
from pddl.requirements import Requirements
from pddl.validation.terms import TermsValidator


class ConstantsDef(_Definition):
"""A set of constants of a PDDL domain."""

def __init__(
self,
requirements: AbstractSet[Requirements],
types: TypesDef,
constants: Optional[Collection[Constant]],
) -> None:
"""Initialize the PDDL constants section validator."""
TermsValidator(requirements, types).check_terms(constants if constants else [])

super().__init__(requirements, types)
self._constants = ensure_set(constants)

@property
def constants(self) -> AbstractSet[Constant]:
"""Get the constants."""
return self._constants
21 changes: 12 additions & 9 deletions pddl/logic/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@

"""This class implements PDDL predicates."""
import functools
from typing import Collection, Dict, Sequence, Set, Tuple
from typing import Collection, Dict, Generator, Sequence, Set, Tuple

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.cache_hash import cache_hash
from pddl.logic.base import Atomic, Formula
Expand All @@ -32,12 +33,10 @@ class _TermsList:

def __init__(self, terms_list: Collection[Term]) -> None:
"""Initialize the terms list."""
self._terms = tuple(terms_list)
self._terms = tuple(self.check_no_duplicate_iterator(terms_list))

self._is_ground: bool = all(isinstance(v, Constant) for v in self._terms)

self._check_terms_consistency()

@property
def terms(self) -> Tuple[Term, ...]:
"""Get the terms sequence."""
Expand All @@ -48,24 +47,28 @@ def is_ground(self) -> bool:
"""Check whether the predicate is ground."""
return self._is_ground

def _check_terms_consistency(self):
@staticmethod
def check_no_duplicate_iterator(
terms: Collection[Term],
) -> Generator[Term, None, None]:
"""
Check that the term sequence have consistent type tags.
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 self._terms:
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} has inconsistent 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=ValueError,
exception_cls=PDDLValidationError,
)
yield term


@cache_hash
Expand Down
4 changes: 2 additions & 2 deletions pddl/logic/terms.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __eq__(self, other) -> bool:

def __hash__(self):
"""Get the hash."""
return hash((Constant, self._name))
return hash((Constant, self._name, self._type_tags))


class Variable(Term):
Expand Down Expand Up @@ -142,4 +142,4 @@ def __eq__(self, other) -> bool:

def __hash__(self) -> int:
"""Get the hash."""
return hash((Variable, self._name))
return hash((Variable, self._name, self._type_tags))
13 changes: 13 additions & 0 deletions pddl/validation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
# 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 package includes validation functions of PDDL domains/problems."""
53 changes: 53 additions & 0 deletions pddl/validation/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#
# 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.
#

"""Base module for validators."""
from typing import AbstractSet, Collection

from pddl.custom_types import name as name_type
from pddl.definitions.base import TypesDef
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import assert_
from pddl.requirements import Requirements


class BaseValidator:
"""Base class for validators."""

def __init__(
self, requirements: AbstractSet[Requirements], types: TypesDef
) -> None:
"""Initialize the validator."""
assert_(type(self) is not BaseValidator)
self._requirements = requirements
self._types = types

@property
def has_typing(self) -> bool:
"""Check if the typing requirement is specified."""
return Requirements.TYPING in self._requirements

def _check_typing_requirement(self, type_tags: Collection[name_type]) -> None:
"""Check that the typing requirement is specified."""
if not self.has_typing and len(type_tags) > 0:
raise PDDLValidationError(
f"typing requirement is not specified, but the following types were used: {type_tags}"
)

def _check_types_are_available(
self, type_tags: Collection[name_type], what: 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}"
)
37 changes: 37 additions & 0 deletions pddl/validation/terms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# 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.
#

"""Module for validator of terms."""
from typing import AbstractSet, Collection

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.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)

def check_terms(self, terms: Collection[Term]) -> None:
"""Check the terms."""
terms_iter = _TermsList.check_no_duplicate_iterator(terms)
for term in terms_iter:
self._check_typing_requirement(term.type_tags)
self._check_types_are_available(term.type_tags, "terms")
16 changes: 15 additions & 1 deletion tests/test_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,25 @@ def test_constants_type_not_available() -> None:

with pytest.raises(
PDDLValidationError,
match=rf"types \['t1'\] of term {re.escape(repr(a))} are not in available types {{'{my_type}'}}",
match=rf"types \['t1'\] of terms are not in available types {{'{my_type}'}}",
):
Domain("test", requirements={Requirements.TYPING}, constants={a}, types=type_set) # type: ignore


def test_constants_duplicates_with_different_types() -> None:
"""Test that when two constants have same name but different types we raise error."""
a1 = Constant("a", type_tag="t1")
a2 = Constant("a", type_tag="t2")

type_set = {"t1": None, "t2": None}

with pytest.raises(
PDDLValidationError,
match=r"Term a occurred twice with different type tags: previous type tags \['t1'\], new type tags \['t2'\]",
):
Domain("test", requirements={Requirements.TYPING}, constants=[a1, a2], types=type_set) # type: ignore


def test_predicate_variable_type_not_available() -> None:
"""Test that when a type of a predicate variable is not declared we raise error."""
x = Variable("a", type_tags={"t1", "t2"})
Expand Down
13 changes: 7 additions & 6 deletions tests/test_logic/test_predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""Test pddl.logic.predicates module."""
import pytest

from pddl.exceptions import PDDLValidationError
from pddl.logic import Predicate
from pddl.logic.predicates import EqualTo
from pddl.logic.terms import Constant, Variable
Expand All @@ -34,9 +35,9 @@ def test_ground_predicate_negative() -> None:
def test_inconsistent_predicate_terms() -> None:
"""Test that terms of a predicate must have consistent typing."""
with pytest.raises(
ValueError,
match=r"Term \?a has inconsistent type tags: previous type tags \['t1', 't2'\], new type tags "
r"\['t3', 't4'\]",
PDDLValidationError,
match=r"Term \?a occurred twice with different type tags: previous type tags \['t1', 't2'\], "
r"new type tags \['t3', 't4'\]",
):
a1, a2 = Variable("a", ["t1", "t2"]), Variable("a", ["t3", "t4"])
Predicate("p", a1, a2)
Expand All @@ -45,9 +46,9 @@ def test_inconsistent_predicate_terms() -> None:
def test_inconsistent_equal_to_terms() -> None:
"""Test that terms of a EqualTo atomic must have consistent typing."""
with pytest.raises(
ValueError,
match=r"Term \?a has inconsistent type tags: previous type tags \['t1', 't2'\], new type tags "
r"\['t3', 't4'\]",
PDDLValidationError,
match=r"Term \?a occurred twice with different type tags: previous type tags \['t1', 't2'\], "
r"new type tags \['t3', 't4'\]",
):
a1, a2 = Variable("a", ["t1", "t2"]), Variable("a", ["t3", "t4"])
EqualTo(a1, a2)

0 comments on commit 3fb63de

Please sign in to comment.