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

Add more consistency checks #84

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
120 changes: 8 additions & 112 deletions pddl/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@

import functools
from collections.abc import Iterable
from typing import AbstractSet, Collection, Dict, Optional, Set, Tuple, cast
from typing import AbstractSet, Collection, Optional, Tuple

from pddl.action import Action
from pddl.custom_types import name as name_type
from pddl.custom_types import namelike, to_names, to_types # noqa: F401
from pddl.definitions.types_def import TypesDef
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import check, ensure, ensure_set, find_cycle
from pddl.helpers.base import check, ensure_set
from pddl.logic import Predicate
from pddl.logic.base import BinaryOp, QuantifiedCondition, UnaryOp
from pddl.logic.effects import AndEffect, Forall, When
from pddl.logic.predicates import DerivedPredicate, EqualTo
from pddl.logic.terms import Term
from pddl.parser.symbols import Symbols
from pddl.requirements import Requirements


Expand All @@ -40,7 +40,7 @@ def validate(condition: bool, message: str = "") -> None:


def _find_inconsistencies_in_typed_terms(
terms: Optional[Collection[Term]], all_types: Set[name_type]
terms: Optional[Collection[Term]], all_types: AbstractSet[name_type]
) -> Optional[Tuple[Term, name_type]]:
"""
Check that the terms in input all have legal types according to the list of available types.
Expand All @@ -60,7 +60,7 @@ def _find_inconsistencies_in_typed_terms(

def _check_types_in_has_terms_objects(
has_terms_objects: Optional[Collection[Predicate]],
all_types: Set[name_type],
all_types: AbstractSet[name_type],
) -> None:
"""Check that the terms in the set of predicates all have legal types."""
if has_terms_objects is None:
Expand All @@ -72,119 +72,15 @@ def _check_types_in_has_terms_objects(
term, type_tag = check_result
raise PDDLValidationError(
f"type {repr(type_tag)} of term {repr(term)} in atomic expression "
f"{repr(has_terms)} is not in available types {all_types}"
)


class Types:
"""A class for representing and managing the types available in a PDDL Domain."""

def __init__(
self,
types: Optional[Dict[namelike, Optional[namelike]]] = None,
requirements: Optional[AbstractSet[Requirements]] = None,
skip_checks: bool = False,
) -> None:
"""Initialize the Types object."""
self._types = to_types(ensure(types, dict()))

self._all_types = self._get_all_types()

if not skip_checks:
self._check_types_dictionary(self._types, ensure_set(requirements))

@property
def raw(self) -> Dict[name_type, Optional[name_type]]:
"""Get the raw types dictionary."""
return self._types

@property
def all_types(self) -> Set[name_type]:
"""Get all available types."""
return self._all_types

def _get_all_types(self) -> Set[name_type]:
"""Get all types supported by the domain."""
if self._types is None:
return set()
result = set(self._types.keys()) | set(self._types.values())
result.discard(None)
return cast(Set[name_type], result)

@classmethod
def _check_types_dictionary(
cls,
type_dict: Dict[name_type, Optional[name_type]],
requirements: AbstractSet[Requirements],
) -> None:
"""
Check the consistency of the types dictionary.

1) Empty types dictionary is correct by definition:
>>> Types._check_types_dictionary({}, set())

2) There are supertypes, but :typing requirement not specified
>>> a, b, c = to_names(["a", "b", "c"])
>>> Types._check_types_dictionary({a: b, b: c}, set())
Traceback (most recent call last):
...
pddl.exceptions.PDDLValidationError: typing requirement is not specified, but types are used: 'b', 'c'

3) The `object` type cannot be a subtype:
>>> a = name_type("a")
>>> Types._check_types_dictionary({name_type("object"): a}, {Requirements.TYPING})
Traceback (most recent call last):
...
pddl.exceptions.PDDLValidationError: object must not have supertypes, but got 'object' is a subtype of 'a'

4) If cycles in the type hierarchy graph are present, an error is raised:
>>> a, b, c = to_names(["a", "b", "c"])
>>> Types._check_types_dictionary({a: b, b: c, c: a}, {Requirements.TYPING})
Traceback (most recent call last):
...
pddl.exceptions.PDDLValidationError: cycle detected in the type hierarchy: a -> b -> c

:param type_dict: the types dictionary
"""
if len(type_dict) == 0:
return

# check typing requirement
supertypes = {t for t in type_dict.values() if t is not None}
if len(supertypes) > 0 and Requirements.TYPING not in requirements:
raise PDDLValidationError(
"typing requirement is not specified, but types are used: '"
+ "', '".join(map(str, sorted(supertypes)))
+ "'"
)

# check `object` type
object_name = name_type(Symbols.OBJECT.value)
if object_name in type_dict and type_dict[object_name] is not None:
object_supertype = type_dict[object_name]
raise PDDLValidationError(
f"object must not have supertypes, but got 'object' is a subtype of '{object_supertype}'"
)

# check cycles
# need to convert type_dict to a dict of sets, because find_cycle() expects a dict of sets
cycle = find_cycle(
{
key: {value} if value is not None else set()
for key, value in type_dict.items()
}
) # type: ignore
if cycle is not None:
raise PDDLValidationError(
"cycle detected in the type hierarchy: " + " -> ".join(cycle)
f"{repr(has_terms)} is not in available types {sorted(all_types)}"
)


class TypeChecker:
"""Implementation of a type checker for domains and problems."""

def __init__(
self, types: Types, requirements: Optional[AbstractSet[Requirements]] = None
self, types: TypesDef, requirements: Optional[AbstractSet[Requirements]] = None
) -> None:
"""Initialize the type checker."""
self._types = types
Expand All @@ -208,7 +104,7 @@ def _check_types_are_available(
"""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)}"
)

@functools.singledispatchmethod # type: ignore
Expand Down
13 changes: 13 additions & 0 deletions pddl/builders/__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 builder classes for PDDL domains and problems."""
171 changes: 171 additions & 0 deletions pddl/builders/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#
# 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 includes the base classes for the PDDL builders."""

from abc import ABC, abstractmethod
from typing import AbstractSet, Generic, Type, TypeVar, Set, Optional, Dict, Callable

from pddl.builders.types_def import TypesDef, MutableTypesDef
from pddl.core import Domain, Problem
from pddl.custom_types import namelike
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import assert_
from pddl.logic.terms import Term
from pddl.requirements import Requirements

T = TypeVar("T", Domain, Problem)


class BaseBuilder(ABC, Generic[T]):
"""A base class for the PDDL builders."""

@abstractmethod
def build(self) -> T:
"""Build the PDDL object."""


class _NoDuplicateList(list):
"""A list that does not allow duplicates."""

def __init__(
self, item_name: str, exception_cls: Type[Exception] = PDDLValidationError
) -> None:
"""Initialize the list."""
super().__init__()
self.__item_name = item_name
self.__exception_cls = exception_cls
# this is for O(1) lookup
self.__elements = set()

def append(self, item) -> None:
"""Append an item to the list."""
if item in self.__elements:
raise PDDLValidationError(f"duplicate {self.__item_name}: '{item}'")
super().append(item)
self.__elements.add(item)

def extend(self, iterable) -> None:
"""Extend the list with an iterable."""
for item in iterable:
self.append(item)

def __contains__(self, item):
"""Check if the list contains an item."""
return item in self.__elements

def get_set(self) -> AbstractSet:
"""Get the set of elements."""
return self.__elements


class _Context:
"""A context for the PDDL builders."""

def __init__(self) -> None:
"""Initialize the context."""
self.__requirements: _NoDuplicateList = _NoDuplicateList("requirement")
self.__types_def: MutableTypesDef = MutableTypesDef()

self.__used_names: Dict[namelike, object] = {}

@property
def requirements(self) -> AbstractSet[Requirements]:
"""Get the requirements."""
return self.__requirements.get_set()

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

@property
def types_def(self) -> MutableTypesDef:
"""Get the types definition."""
return self.__types_def

def add_requirement(self, requirement: Requirements) -> None:
"""Add a requirement to the domain."""
self.__requirements.append(requirement)

def add_type(
self, child_type: namelike, parent_type: Optional[namelike] = None
) -> None:
"""Add a type to the domain."""
self.check_name_not_already_used(child_type, "type")
self.check_name_not_already_used(parent_type, "type") if parent_type is not None else None
self.check_typing_requirement_for_types(child_type, parent_type)

self.__types_def.add_type(child_type, parent_type)

self.add_used_name(child_type, "type")
self.add_used_name(parent_type, "type") if parent_type is not None else None

def add_used_name(self, name: namelike, obj: object) -> None:
"""Add a name to the used names."""
self.__used_names[name] = obj

def get_used_name(self, name: namelike) -> Optional[object]:
"""Add a name to the used names."""
return self.__used_names.get(name)

def check_typing_requirement_for_types(
self, child_type: namelike, parent_type: Optional[namelike] = None
) -> None:
"""Check that the typing requirement is specified."""
if not self.has_typing:
raise PDDLValidationError(
f"typing requirement is not specified, but the following types were used: {child_type}"
+ (f" -> {parent_type}" if parent_type else "")
)

def check_name_not_already_used(self, new_name: namelike, new_object: object) -> None:
"""Check that the name is not already used."""
if new_name in self.__used_names:
raise PDDLValidationError(
f"name '{new_name}' of object '{new_object}' is already used for '{self.__used_names[new_name]}'"
)

def check_types_are_available(self, term: Term) -> None:
"""Check that the types of a term are available in the domain."""
if not self.types_def.are_types_available(term.type_tags):
raise PDDLValidationError(
f"types {sorted(term.type_tags)} of term '{term}' are not in available types {self.types_def.sorted_all_types}"
)


class _Definition:
"""Abstract class for a PDDL definition."""

def __init__(
self, context: _Context
) -> None:
"""Initialize the PDDL definition."""
assert_(type(self) is not _Definition)
self.__context = context

@property
def _context(self) -> _Context:
"""Get the context."""
return self.__context

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

def _check_typing_requirement_for_term(self, term: Term) -> None:
"""Check that the typing requirement is specified for a term."""
if not self.has_typing and len(term.type_tags) > 0:
raise PDDLValidationError(
f"typing requirement is not specified, but the following types for term '{term}' were used: {term.type_tags}"
)
40 changes: 40 additions & 0 deletions pddl/builders/constants_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
# 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, Sequence, cast

from pddl.builders.base import _Definition, _Context
from pddl.builders.terms_list import TermsValidator
from pddl.logic import Constant


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

def __init__(self, context: _Context) -> None:
"""Initialize the PDDL constants section validator."""
super().__init__(context)
self._terms_validator = TermsValidator(
no_duplicates=True, must_be_instances_of=Constant
)

def add_constant(self, constant: Constant) -> None:
"""Add a constant."""
self._check_typing_requirement_for_term(constant)
self._context.check_types_are_available(constant)
self._terms_validator.add_term(constant)

@property
def constants(self) -> AbstractSet[Constant]:
"""Get the constants."""
return frozenset(cast(Sequence[Constant], self._terms_validator.terms))
Loading