# Copyright 2021-2023 AIPlan4EU project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""This module defines all the types."""
import unified_planning
from typing import Iterator, Optional, cast
from unified_planning.exceptions import UPProblemDefinitionError, UPTypeError
from abc import ABC
from fractions import Fraction
[docs]
class Type(ABC):
"""Base class for representing a `Type`."""
[docs]
def is_bool_type(self) -> bool:
"""Returns `True` iff is `boolean Type`."""
return False
[docs]
def is_user_type(self) -> bool:
"""Returns `True` iff is a `user Type`."""
return False
[docs]
def is_real_type(self) -> bool:
"""Returns `True` iff is `real` `Type`."""
return False
[docs]
def is_int_type(self) -> bool:
"""Returns `True` iff is `integer Type`."""
return False
[docs]
def is_time_type(self) -> bool:
"""Returns `True` iff is `time Type`."""
return False
[docs]
def is_movable_type(self) -> bool:
"""Returns `True` iff is `movable Type`."""
return False
[docs]
def is_configuration_type(self) -> bool:
"""Returns `True` iff is `configuration Type`."""
return False
[docs]
def is_compatible(self, t_right: "Type") -> bool:
"""
Returns `True` if the `Type` `t_right` can be assigned to a :class:`~unified_planning.model.Fluent` that which :func:`type <unified_planning.model.Fluent.type>` is self.
Note that types compatibility is not a symmetric property.
:param t_right: The type tested for compatibility.
:return: `True` if something, generally an :class:`~unified_planning.model.Object` or an :func:`action parameters <unified_planning.model.Action.parameters>`,
with the given `type` can always be assigned to a `fluent` with this `type`.
"""
return is_compatible_type(self, t_right)
class _BoolType(Type):
"""Represents the boolean type."""
def __repr__(self) -> str:
return "bool"
def is_bool_type(self) -> bool:
"""Returns true iff is boolean type."""
return True
class _TimeType(Type):
"""Represent the type for an absolute Time"""
def __repr__(self) -> str:
return "time"
def is_time_type(self) -> bool:
"""Returns true iff is boolean type."""
return True
class _UserType(Type):
"""Represents the user type."""
def __init__(self, name: str, father: Optional[Type] = None):
assert isinstance(name, str)
Type.__init__(self)
self._name = name
if father is not None and (not father.is_user_type()):
raise UPTypeError("father field of a UserType must be a UserType.")
self._father = father
def __repr__(self) -> str:
return (
self._name
if self._father is None
else f"{self._name} - {cast(_UserType, self._father).name}"
)
@property
def name(self) -> str:
"""Returns the type name."""
return self._name
@property
def father(self) -> Optional[Type]:
"""Returns the type's father."""
return self._father
@property
def ancestors(self) -> Iterator[Type]:
"""Returns all the ancestors of this type, including itself."""
type: Optional[Type] = self
while type is not None:
yield type
type = cast(_UserType, type).father
def is_user_type(self) -> bool:
"""Returns true iff is a user type."""
return True
def is_subtype(self, t: Type) -> bool:
"""
Returns true iff the given type is a subtype of the given type.
Note: t is a subtype of self if t is in the self's ancestors (or if t and self are the same type).
:param t: The type tested for subtyping.
:return: True if the given type is a subtype of self.
"""
assert t.is_user_type(), "Subtyping is only available for UserTypes."
p: Optional[Type] = self
while p is not None:
if p == t:
return True
p = cast(_UserType, p).father
return False
class _IntType(Type):
"""Represents an Integer type. The given bounds are semantically bounding for the planners."""
def __init__(
self, lower_bound: Optional[int] = None, upper_bound: Optional[int] = None
):
Type.__init__(self)
assert lower_bound is None or isinstance(
lower_bound, int
), "typing not respected"
assert upper_bound is None or isinstance(
upper_bound, int
), "typing not respected"
self._lower_bound = lower_bound
self._upper_bound = upper_bound
def __repr__(self) -> str:
b = []
if (not self.lower_bound is None) or (not self.upper_bound is None):
b.append("[")
b.append("-inf" if self.lower_bound is None else str(self.lower_bound))
b.append(", ")
b.append("inf" if self.upper_bound is None else str(self.upper_bound))
b.append("]")
return "integer" + "".join(b)
@property
def lower_bound(self) -> Optional[int]:
"""Returns this type lower bound."""
return self._lower_bound
@property
def upper_bound(self) -> Optional[int]:
"""Returns this type upper bound."""
return self._upper_bound
def is_int_type(self) -> bool:
return True
class _RealType(Type):
"""Represents a Real type. The given bounds are semantically bounding for the planners."""
def __init__(
self,
lower_bound: Optional[Fraction] = None,
upper_bound: Optional[Fraction] = None,
):
Type.__init__(self)
assert lower_bound is None or isinstance(
lower_bound, Fraction
), "typing not respected"
assert upper_bound is None or isinstance(
upper_bound, Fraction
), "typing not respected"
self._lower_bound = lower_bound
self._upper_bound = upper_bound
def __repr__(self) -> str:
b = []
if (not self.lower_bound is None) or (not self.upper_bound is None):
b.append("[")
b.append("-inf" if self.lower_bound is None else str(self.lower_bound))
b.append(", ")
b.append("inf" if self.upper_bound is None else str(self.upper_bound))
b.append("]")
return "real" + "".join(b)
@property
def lower_bound(self) -> Optional[Fraction]:
"""Returns this type lower bound."""
return self._lower_bound
@property
def upper_bound(self) -> Optional[Fraction]:
"""Returns this type upper bound."""
return self._upper_bound
def is_real_type(self) -> bool:
return True
BOOL = _BoolType()
TIME = _TimeType()
def domain_size(
objects_set: "unified_planning.model.mixins.ObjectsSetMixin",
typename: "unified_planning.model.types.Type",
) -> int:
"""
Returns the domain size of the given type; the domain size is the number of values
that an element with the given `Type` can have.
:param object_set: The :class:`~unified_planning.model.AbstractProblem` instance containing the :class:`Objects <unified_planning.model.Object>`.
:param typename: The type of which the domain size (in the given `object_set`) is returned.
:return: The number of values that the given `Type` can have in the given `Problem`.
"""
if typename.is_bool_type():
return 2
elif typename.is_user_type():
return len(list(objects_set.objects(typename)))
elif typename.is_int_type():
typename = cast(_IntType, typename)
lb = typename.lower_bound
ub = typename.upper_bound
if lb is None or ub is None:
raise UPProblemDefinitionError("Parameter not groundable!")
return ub - lb + 1
else:
raise UPProblemDefinitionError("Parameter not groundable!")
def domain_item(
objects_set: "unified_planning.model.mixins.ObjectsSetMixin",
typename: "unified_planning.model.types.Type",
idx: int,
) -> "unified_planning.model.fnode.FNode":
"""
Returns the ith element of the given `type` in the given `problem`.
:param object_set: The :class:`~unified_planning.model.AbstractProblem` instance containing the :class:`Objects <unified_planning.model.Object>`.
:param typename: The type of which the `idx` element is returned.
:param idx: The index of the domain item that is returned
:return: The `idx` domain item of the given `Type` in the domain of the given `Problem` instance.
"""
if typename.is_bool_type():
return objects_set.environment.expression_manager.Bool(idx == 0)
elif typename.is_user_type():
return objects_set.environment.expression_manager.ObjectExp(
list(objects_set.objects(typename))[idx]
)
elif typename.is_int_type():
typename = cast(_IntType, typename)
lb = typename.lower_bound
ub = typename.upper_bound
if lb is None or ub is None:
raise UPProblemDefinitionError("Parameter not groundable!")
return objects_set.environment.expression_manager.Int(lb + idx)
else:
raise UPProblemDefinitionError("Parameter not groundable!")
def is_compatible_type(
t_left: "Type",
t_right: "Type",
) -> bool:
"""
Returns True if the type t_right can be assigned to a typed up object that has type t_left.
:param t_left: the target type for the assignment.
:param t_right: the type of the element that wants to be assigned to the element of type t_left.
:return: True if the element of type t_left can be assigned to the element of type t_right; False otherwise.
"""
if t_left == t_right:
return True
if t_left.is_user_type() and t_right.is_user_type():
assert isinstance(t_left, _UserType) and isinstance(t_right, _UserType)
# compatible if t_right is a subclass of t_left
return t_left in t_right.ancestors
if not (
(t_left.is_int_type() and t_right.is_int_type())
or (t_left.is_real_type() and t_right.is_real_type())
or (t_left.is_real_type() and t_right.is_int_type())
):
return False
assert isinstance(t_left, _IntType) or isinstance(t_left, _RealType)
assert isinstance(t_right, _IntType) or isinstance(t_right, _RealType)
left_lower = -float("inf") if t_left.lower_bound is None else t_left.lower_bound
left_upper = float("inf") if t_left.upper_bound is None else t_left.upper_bound
right_lower = -float("inf") if t_right.lower_bound is None else t_right.lower_bound
right_upper = float("inf") if t_right.upper_bound is None else t_right.upper_bound
if right_upper < left_lower or right_lower > left_upper:
return False
else:
return True