Skip to content

Commit

Permalink
Merge pull request #92 from AI-Planning/feat/numerical-fluents
Browse files Browse the repository at this point in the history
Support for numeric fluents and action costs
  • Loading branch information
francescofuggitti committed Oct 18, 2023
2 parents fe1013b + a18263c commit 137cb67
Show file tree
Hide file tree
Showing 139 changed files with 21,536 additions and 128 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ requirements:
- [x] `:quantified-preconditions`
- [x] `:conditional-effects`
- [ ] `:fluents`
- [ ] `:numeric-fluents`
- [x] `:numeric-fluents`
- [x] `:non-deterministic` (see [6th IPC: Uncertainty Part](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.163.7140&rep=rep1&type=pdf))
- [x] `:adl`
- [ ] `:durative-actions`
Expand All @@ -192,7 +192,7 @@ requirements:
- [ ] `:timed-initial-literals`
- [ ] `:preferences`
- [ ] `:constraints`
- [ ] `:action-costs`
- [x] `:action-costs`

## Development

Expand Down
88 changes: 88 additions & 0 deletions pddl/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@
from typing import AbstractSet, Collection, Dict, Optional, Set, Tuple, cast

from pddl.action import Action
from pddl.custom_types import _check_not_a_keyword # noqa: F401
from pddl.custom_types import name as name_type
from pddl.custom_types import namelike, to_names, to_types # noqa: F401
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import check, ensure, ensure_set, find_cycle
from pddl.logic import Predicate
from pddl.logic.base import BinaryOp, QuantifiedCondition, UnaryOp
from pddl.logic.effects import AndEffect, Forall, When
from pddl.logic.functions import (
BinaryFunction,
FunctionExpression,
NumericFunction,
NumericValue,
)
from pddl.logic.predicates import DerivedPredicate, EqualTo
from pddl.logic.terms import Term
from pddl.parser.symbols import Symbols
Expand Down Expand Up @@ -234,6 +241,21 @@ def _(self, predicate: Predicate) -> None:
"""Check types annotations of a PDDL predicate."""
self.check_type(predicate.terms)

@check_type.register
def _(self, function: NumericFunction) -> None:
"""Check types annotations of a PDDL numeric function."""
self.check_type(function.terms)

@check_type.register
def _(self, _: NumericValue) -> None:
"""Check types annotations of a PDDL numeric value operator."""
return None

@check_type.register
def _(self, binary_function: BinaryFunction) -> None:
"""Check types annotations of a PDDL numeric binary operator."""
self.check_type(binary_function.operands)

@check_type.register
def _(self, equal_to: EqualTo) -> None:
"""Check types annotations of a PDDL equal-to atomic formula."""
Expand Down Expand Up @@ -285,3 +307,69 @@ def _(self, action: Action) -> None:
self.check_type(action.parameters)
self.check_type(action.precondition)
self.check_type(action.effect)


class Functions:
"""A class for representing and managing the numeric functions available in a PDDL Domain."""

def __init__(
self,
functions: Optional[Collection[FunctionExpression]] = None,
requirements: Optional[AbstractSet[Requirements]] = None,
skip_checks: bool = False,
) -> None:
"""Initialize the Functions object."""
self._functions = ensure(functions, dict())
self._all_function = self._get_all_functions()

if not skip_checks:
self._check_total_cost(self._functions, ensure_set(requirements))

@property
def raw(self) -> Dict[NumericFunction, Optional[name_type]]:
"""Get the raw functions' dictionary."""
return self._functions

@property
def all_functions(self) -> Set[NumericFunction]:
"""Get all available functions."""
return self._all_function

def _get_all_functions(self) -> Set[NumericFunction]:
"""Get all function supported by the domain."""
if self._functions is None:
return set()
result = set(self._functions.keys()) | set(self._functions.values())
result.discard(None)
return cast(Set[NumericFunction], result)

@classmethod
def _check_total_cost(
cls,
function_dict: Dict[NumericFunction, Optional[name_type]],
requirements: AbstractSet[Requirements],
) -> None:
"""Check consistency of total-cost function with the requirements."""
if bool(function_dict):
if any(f.name == Symbols.TOTAL_COST.value for f in {*function_dict}):
validate(
Requirements.ACTION_COSTS in requirements,
f"action costs requirement is not specified, but the {Symbols.TOTAL_COST.value} "
f"function is specified.",
)
if any(
isinstance(f, FunctionExpression)
and not f.name == Symbols.TOTAL_COST.value
for f in function_dict.keys()
):
validate(
Requirements.NUMERIC_FLUENTS
in _extend_domain_requirements(requirements),
"numeric-fluents requirement is not specified, but numeric fluents are specified.",
)
else:
validate(
Requirements.NUMERIC_FLUENTS
in _extend_domain_requirements(requirements),
"numeric-fluents requirement is not specified, but numeric fluents are specified.",
)
29 changes: 25 additions & 4 deletions pddl/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import AbstractSet, Collection, Dict, Optional, Tuple, cast

from pddl._validation import (
Functions,
TypeChecker,
Types,
_check_types_in_has_terms_objects,
Expand All @@ -28,6 +29,7 @@
from pddl.custom_types import namelike, parse_name, to_names, to_types # noqa: F401
from pddl.helpers.base import assert_, check, ensure, ensure_set
from pddl.logic.base import And, Formula, is_literal
from pddl.logic.functions import FunctionExpression, Metric, NumericFunction
from pddl.logic.predicates import DerivedPredicate, Predicate
from pddl.logic.terms import Constant
from pddl.requirements import Requirements
Expand All @@ -46,6 +48,7 @@ def __init__(
derived_predicates: Optional[
Collection[DerivedPredicate]
] = None, # TODO cannot be empty
functions: Optional[Collection[FunctionExpression]] = None,
actions: Optional[Collection["Action"]] = None,
):
"""
Expand All @@ -57,6 +60,8 @@ def __init__(
types is a dictionary mapping a type name to its ancestor.
:param constants: the constants.
:param predicates: the predicates.
:param functions: the functions.
functions is a dictionary mapping a function to its type.
:param derived_predicates: the derived predicates.
:param actions: the actions.
"""
Expand All @@ -67,15 +72,16 @@ def __init__(
self._predicates = ensure_set(predicates)
self._derived_predicates = ensure_set(derived_predicates)
self._actions = ensure_set(actions)
self._functions = Functions(functions, self._requirements)

self._check_consistency()

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)
type_checker = TypeChecker(self._types, self.requirements)
type_checker.check_type(self._constants)
type_checker.check_type(self._predicates)
type_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()

Expand Down Expand Up @@ -108,6 +114,11 @@ def predicates(self) -> AbstractSet[Predicate]:
"""Get the predicates."""
return self._predicates

@property
def functions(self) -> Dict[NumericFunction, Optional[name_type]]:
"""Get the functions."""
return self._functions.raw

@property
def derived_predicates(self) -> AbstractSet[DerivedPredicate]:
"""Get the derived predicates."""
Expand All @@ -132,6 +143,7 @@ def __eq__(self, other):
and self.types == other.types
and self.constants == other.constants
and self.predicates == other.predicates
and self.functions == other.functions
and self.derived_predicates == other.derived_predicates
and self.actions == other.actions
)
Expand All @@ -149,6 +161,7 @@ def __init__(
objects: Optional[Collection["Constant"]] = None,
init: Optional[Collection[Formula]] = None,
goal: Optional[Formula] = None,
metric: Optional[Metric] = None,
):
"""
Initialize the PDDL problem.
Expand All @@ -160,6 +173,7 @@ def __init__(
:param objects: the set of objects.
:param init: the initial condition.
:param goal: the goal condition.
:param metric: the metric.
"""
self._name = parse_name(name)
self._domain: Optional[Domain]
Expand All @@ -173,6 +187,7 @@ def __init__(
self._objects: AbstractSet[Constant] = ensure_set(objects)
self._init: AbstractSet[Formula] = ensure_set(init)
self._goal: Formula = ensure(goal, And())
self._metric: Optional[Metric] = metric
validate(
all(map(is_literal, self.init)),
"Not all formulas of initial condition are literals!",
Expand Down Expand Up @@ -298,6 +313,11 @@ def goal(self) -> Formula:
"""Get the goal."""
return self._goal

@property
def metric(self) -> Optional[Metric]:
"""Get the metric."""
return self._metric

def __eq__(self, other):
"""Compare with another object."""
return (
Expand All @@ -309,4 +329,5 @@ def __eq__(self, other):
and self.objects == other.objects
and self.init == other.init
and self.goal == other.goal
and self.metric == other.metric
)
18 changes: 18 additions & 0 deletions pddl/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ def parse_type(s: str) -> name:
return name(s)


def parse_function(s: str) -> name:
"""
Parse a function name from a string.
It performs two validations:
- the function name is not a keyword;
- the function name is a valid name, i.e. it matches the name regular expression.
The function name 'total-cost' is allowed.
:param s: the input string to be parsed
:return: the parsed name
"""
_check_not_a_keyword(s, "name", ignore={Symbols.TOTAL_COST.value})
return name(s)


def to_names(names: Collection[namelike]) -> List[name]:
"""From name-like sequence to list of names."""
return list(map(parse_name, names))
Expand All @@ -100,6 +117,7 @@ def to_types(names: Dict[namelike, Optional[namelike]]) -> Dict[name, Optional[n
def _is_a_keyword(word: str, ignore: Optional[AbstractSet[str]] = None) -> bool:
"""Check that the word is not a keyword."""
ignore_set = ensure_set(ignore)
# we remove the TOTAL_COST because it is not a keyword but a special function
return word not in ignore_set and word in ALL_SYMBOLS


Expand Down
Loading

0 comments on commit 137cb67

Please sign in to comment.