Skip to content

Commit

Permalink
temp: work in progress: builder pattern for validation
Browse files Browse the repository at this point in the history
  • Loading branch information
marcofavorito committed Jul 2, 2023
1 parent 6ba0257 commit e9cd01d
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 331 deletions.
2 changes: 1 addition & 1 deletion pddl/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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.base import TypesDef
from pddl.definitions.types_def import TypesDef
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import check, ensure_set
from pddl.logic import Predicate
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."""
74 changes: 74 additions & 0 deletions pddl/builders/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#
# 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

from pddl.builders.types_def import TypesDef
from pddl.core import Domain, Problem
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import assert_
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


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

def __init__(
self, requirements: AbstractSet[Requirements], types: TypesDef
) -> None:
"""Initialize the PDDL definition."""
assert_(type(self) is not _Definition)
self._requirements = requirements
self._types = types
36 changes: 36 additions & 0 deletions pddl/builders/constants_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# 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.terms_list import TermsValidator
from pddl.logic import Constant


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

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

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

@property
def constants(self) -> AbstractSet[Constant]:
"""Get the constants."""
return frozenset(cast(Sequence[Constant], self._terms_validator.terms))
86 changes: 86 additions & 0 deletions pddl/builders/domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Callable, Collection, List, Optional

from pddl.builders.base import BaseBuilder, _NoDuplicateList
from pddl.builders.constants_def import ConstantsDef
from pddl.builders.types_def import MutableTypesDef
from pddl.custom_types import namelike
from pddl.exceptions import PDDLValidationError
from pddl.logic import Constant
from pddl.logic.terms import Term
from pddl.requirements import Requirements


class DomainBuilder(BaseBuilder):
"""A builder for PDDL domains."""

def __init__(self, name: str):
"""Initialize the domain builder."""
self.__name = name
self.__requirements: List[Requirements] = _NoDuplicateList("requirement")
self.__types_def = MutableTypesDef()
self.__constants_def: ConstantsDef = ConstantsDef()

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

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

def add_type(
self, child_type: namelike, parent_type: Optional[namelike] = None
) -> "DomainBuilder":
"""Add a type to the domain."""
self._check_typing_requirement_for_types(child_type, parent_type)
self.__types_def.add_type(child_type, parent_type)
return self

def add_constant(self, constant: Constant) -> "DomainBuilder":
"""Add a constant to the domain."""
self._check_typing_requirement_for_term(constant)
self._check_types_are_available(constant)
self.__constants_def.add_constant(constant)
return self

def build(self) -> "T":
pass

# def build(self) -> Domain:
# """Build the domain."""
# return Domain(
# name=self.__name,
# requirements=self.__requirements,
# types=self.types,
# constants=self.__constants,
# predicates=self.predicates,
# functions=self.functions,
# actions=self.actions,
# axioms=self.axioms,
# )

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_typing_requirement_for_term(self, term: Term) -> None:
"""Check that the typing requirement is specified."""
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}"
)

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}"
)
104 changes: 44 additions & 60 deletions pddl/validation/terms.py → pddl/builders/terms_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,16 @@
# https://opensource.org/licenses/MIT.
#

"""Module for validator of terms."""
from functools import partial
from typing import (
AbstractSet,
Collection,
Dict,
Generator,
Mapping,
Optional,
Type,
Union,
)
"""Module for validator of terms lists."""
from typing import Collection, Dict, List, Optional, Sequence, Type, Union

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 check
from pddl.logic.terms import Constant, Term, Variable, _print_tag_set
from pddl.requirements import Requirements
from pddl.validation.base import BaseValidator


class TermsValidator(BaseValidator):
class TermsValidator:
"""
Class for validator of terms.
Expand All @@ -41,69 +28,51 @@ class TermsValidator(BaseValidator):

def __init__(
self,
requirements: AbstractSet[Requirements],
types: TypesDef,
must_be_instances_of: Optional[Union[Type[Constant], Type[Variable]]] = None,
no_duplicates: bool = False,
):
"""Initialize the validator."""
super().__init__(requirements, types)

# if none, then we don't care if constant or variable
self._allowed_superclass = must_be_instances_of
self._no_duplicates = no_duplicates

def check_terms_consistency(self, terms: Collection[Term]):
"""
Check that there are no duplicates.
# a dictionary from name to Term, for fast lookup
self._seen: Dict[name_type, Term] = {}

This is the non-iterative version of '_check_terms_consistency_iterator'.
"""
# consume the iterator
list(self._check_terms_consistency_iterator(terms))
# the full list of terms
self._terms: List[Term] = []

def _check_terms_consistency_iterator(
self, terms: Collection[Term]
) -> Generator[Term, None, None]:
"""
Iterate over terms and check that terms with the same name must have the same type tags.
@property
def terms(self) -> Sequence[Term]:
"""Get the terms."""
return self._terms

In particular:
- if no_duplicates=Term there cannot be terms with the same name (variable or constant);
- 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, Term] = {}
for term in terms:
self._check_already_seen_term(term, seen)
self._check_same_term_has_same_type_tags(term, seen)
self._check_term_type(term, term_type=self._allowed_superclass)
yield term
seen[term.name] = term
@classmethod
def check_terms(cls, terms: Collection[Term]):
"""Check that there are no duplicates."""
TermsValidator().add_terms(terms)

def _check_already_seen_term(self, term: Term, seen: Mapping[name_type, Term]):
def _check_already_seen_term(self, term: Term):
"""Check whether a term has been already seen earlier in the terms list."""
if self._no_duplicates and term.name in seen:
if self._no_duplicates and term.name in self._seen:
same_name_but_different_type = type(term) is not type( # noqa: E721
seen[term.name]
self._seen[term.name]
)
check(
same_name_but_different_type,
f"Term '{term}' occurred twice in the same list of terms",
exception_cls=PDDLValidationError,
)

@classmethod
def _check_same_term_has_same_type_tags(
cls, term: Term, seen: Dict[name_type, Term]
) -> None:
def _check_same_term_has_same_type_tags(self, term: Term) -> None:
"""
Check if the term has already been seen and, if so, that it has the same type tags.
This is an auxiliary method to simplify the implementation of '_check_terms_consistency_iterator'.
"""
if term.name in seen:
expected_type_tags = seen[term.name].type_tags
if term.name in self._seen:
expected_type_tags = self._seen[term.name].type_tags
actual_type_tags = set(term.type_tags)
check(
expected_type_tags == actual_type_tags,
Expand All @@ -127,16 +96,31 @@ def _check_term_type(
exception_cls=PDDLValidationError,
)

def check_terms(self, terms: Collection[Term]) -> None:
"""Check the 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, partial(self._terms_to_string, terms)
)
def add_terms(self, terms: Collection[Term]) -> None:
"""Perform consistency checks and add a list of terms."""
for term in terms:
self.add_term(term)

def check_term(self, term: Term) -> None:
"""
Perform consistency checks against a single term.
In particular:
- if no_duplicates=Term there cannot be terms with the same name (variable or constant);
- 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.
"""
self._check_already_seen_term(term)
self._check_same_term_has_same_type_tags(term)
self._check_term_type(term, term_type=self._allowed_superclass)

@classmethod
def _terms_to_string(cls, terms: Collection[Term]) -> str:
"""Convert terms to string for error messages."""
return "terms ['" + "', '".join(map(str, terms)) + "']"

def add_term(self, term: Term) -> None:
"""Add a single term."""
self.check_term(term)
self._seen[term.name] = term
self._terms.append(term)
Loading

0 comments on commit e9cd01d

Please sign in to comment.