# 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.
#
from unified_planning.environment import Environment
from unified_planning.model.fnode import FNode
from unified_planning.model.expression import (
NumericConstant,
uniform_numeric_constant,
TimeExpression,
)
from abc import ABC
from enum import Enum, auto
from fractions import Fraction
from typing import Union, Optional
class TimepointKind(Enum):
"""
`Enum` representing all the possible :func:`kinds <unified_planning.model.Timepoint.kind>` of a :class:`~unified_planning.model.Timepoint`.
The `kind` of a Timepoint defines it's semantic:
GLOBAL_START => At the start of the `Plan`
GLOBAL_END => At the end of the `Plan`
START => At the start of the `Action`
END => At the end of the `Action`
"""
GLOBAL_START = auto()
GLOBAL_END = auto()
START = auto()
END = auto()
[docs]
class Timepoint:
"""Temporal point of interest, one of:
- global start: temporal origin (time 0) at which the initial state is defined
- global end: plan horizon, at which the plan goals must hold.
- start time or end time of an action, activity or task/method.
Used to define the point in the time from which a :class:`~unified_planning.model.timing.Timing` is considered."""
def __init__(self, kind: TimepointKind, container: Optional[str] = None):
"""
Creates a new :class:`Timepoint`.
It is typically used to refer to:
- the start/end of the containing action or method, or
- to the start/end of a subtasks in a method
Parameters
----------
kind: TimepointKind
Kind of the timepoint.
container: Optional[str]
Identifier of the container in which the timepoint is defined.
If not set, then a start/end timepoint refers to the enclosing action or method.
"""
assert container is None or isinstance(container, str)
self._kind = kind
self._container = container
def __repr__(self):
if (
self._kind == TimepointKind.GLOBAL_START
or self._kind == TimepointKind.START
):
qualifier = "start"
else:
qualifier = "end"
if self._container is None:
return qualifier
else:
return f"{qualifier}({self._container})"
def __eq__(self, oth: object) -> bool:
if isinstance(oth, Timepoint):
return self._kind == oth._kind and self._container == oth._container
else:
return False
def __hash__(self) -> int:
return hash(self._kind) + hash(self._container)
def __add__(self, delay: Union[int, Fraction]) -> "Timing":
return Timing(delay, self)
def __sub__(self, delay: Union[int, Fraction]) -> "Timing":
return Timing(-delay, self)
@property
def kind(self) -> TimepointKind:
"""Returns the `kind` of this :class:`Timepoint`; the `kind` defines the semantic of the `Timepoint`."""
return self._kind
@property
def container(self):
"""Returns the `container` in which this `Timepoint` is defined or `None` if it refers to the enclosing `action/method`."""
return self._container
[docs]
class Timing:
"""Time defined relatively to a :class:`Timepoint`.
Class that used a :class:`~unified_planning.model.timing.Timepoint` to define from when this `Timing` is considered and a :func:`delay <unified_planning.model.Timing.delay>`,
representing the distance from the given `Timepoint`.
For instance:
A `GLOBAL_START Timepoint` with a `delay` of `5` means `5` units of time after the initial state.
"""
def __init__(self, delay: NumericConstant, timepoint: Timepoint):
self._timepoint = timepoint
self._delay = uniform_numeric_constant(delay)
def __repr__(self):
if self._delay == 0:
return f"{self._timepoint}"
elif self._delay < 0:
return f"{self._timepoint} - {-self._delay}"
else:
return f"{self._timepoint} + {self._delay}"
def __eq__(self, oth: object) -> bool:
if isinstance(oth, Timing):
return self._delay == oth._delay and self._timepoint == oth._timepoint
else:
return False
def __hash__(self) -> int:
return hash(self._delay) ^ hash(self._timepoint)
def __add__(self, delay: Union[int, Fraction]) -> "Timing":
return Timing(self.delay + delay, self.timepoint)
def __sub__(self, delay: Union[int, Fraction]) -> "Timing":
return Timing(self.delay - delay, self.timepoint)
@property
def delay(self) -> Union[int, Fraction]:
"""Returns the `delay` set for this `Timing` from the `timepoint`."""
return self._delay
@property
def timepoint(self) -> Timepoint:
"""Returns the `Timepoint` from which this `Timing` is considered."""
return self._timepoint
[docs]
def is_global(self) -> bool:
"""
Returns `True` if this `Timing` refers to the global timing in the `Plan` and not the `start/end` of an :class:`~unified_planning.model.Action`,
`False` otherwise.
"""
return (
self._timepoint.kind == TimepointKind.GLOBAL_START
or self._timepoint.kind == TimepointKind.GLOBAL_END
)
[docs]
def is_from_start(self) -> bool:
"""Returns `True` if this `Timing` is from the start, `False` if it is from the end."""
return (
self._timepoint.kind == TimepointKind.START
or self._timepoint.kind == TimepointKind.GLOBAL_START
)
[docs]
def is_from_end(self) -> bool:
"""Returns `True` if this `Timing` is from the end, `False` if it is from the start."""
return not self.is_from_start()
[docs]
@staticmethod
def from_time(time: TimeExpression) -> "Timing":
"""Converts any supported time expression into its canonical Timing representation."""
if (
isinstance(time, int)
or isinstance(time, float)
or isinstance(time, Fraction)
):
return GlobalStartTiming() + time
elif isinstance(time, Timepoint):
return Timing(timepoint=time, delay=0)
else:
assert isinstance(time, Timing)
return time
def StartTiming(delay: NumericConstant = 0, container: Optional[str] = None) -> Timing:
"""
Returns the start timing of an :class:`~unified_planning.model.Action`.
Created with a delay > 0 represents "delay" time
after the start of the `Action`.
For example, action starts at time 5:
`StartTiming() = 5`
`StartTiming(3) = 5+3 = 8`.
:param delay: The delay from the start of an `action`.
:param container: Identifier of the container in which the `timepoint` is defined.
If not set, then refers to the enclosing `Action or method`.
:return: The created `Timing`.
"""
return Timing(delay, Timepoint(TimepointKind.START, container=container))
def EndTiming(container: Optional[str] = None) -> Timing:
"""
Returns the end timing of an :class:`~unified_planning.model.Action`.
For example, `Action` ends at time 10:
`EndTiming() = 10`
`EndTiming() - 4 = 10 - 4 = 6`.
:param container: Identifier of the container in which the `Timepoint` is defined.
If not set, then refers to the enclosing `action or method`.
:return: The created `Timing`.
"""
return Timing(delay=0, timepoint=Timepoint(TimepointKind.END, container=container))
def GlobalStartTiming(delay: NumericConstant = 0):
"""
Represents the absolute `Timing`.
Created with a delay > 0 represents `delay` time
after the start of the execution.
:param delay: The delay from the start of the `Plan`.
:return: The created `Timing`.
"""
return Timing(delay, Timepoint(TimepointKind.GLOBAL_START))
def GlobalEndTiming():
"""
Represents the end `Timing` of an execution.
Created with a delay > 0 represents "delay" time
before the end of the execution.
:param delay: The delay from the start of the `Plan`.
:return: The created `Timing`.
"""
return Timing(delay=0, timepoint=Timepoint(TimepointKind.GLOBAL_END))
[docs]
class Interval:
"""Class that defines an `interval` with 2 :class:`expressions <unified_planning.model.FNode>` as bounds."""
def __init__(
self,
lower: FNode,
upper: FNode,
is_left_open: bool = False,
is_right_open: bool = False,
):
self._lower = lower
self._upper = upper
self._is_left_open = is_left_open
self._is_right_open = is_right_open
assert (
lower.environment == upper.environment
), "Interval s boundaries expression can not have different environments"
def __repr__(self) -> str:
if self.is_left_open():
left_bound = "("
else:
left_bound = "["
if self.is_right_open():
right_bound = ")"
else:
right_bound = "]"
return f"{left_bound}{str(self.lower)}, {str(self.upper)}{right_bound}"
def __eq__(self, oth: object) -> bool:
if isinstance(oth, Interval):
return (
self._lower == oth._lower
and self._upper == oth._upper
and self._is_left_open == oth._is_left_open
and self._is_right_open == oth._is_right_open
)
else:
return False
def __hash__(self) -> int:
res = hash(self._lower) + hash(self._upper)
if self._is_left_open:
res ^= hash("is_left_open")
if self._is_right_open:
res ^= hash("is_right_open")
return res
@property
def lower(self) -> FNode:
"""Returns the `Interval's` lower bound."""
return self._lower
@property
def upper(self) -> FNode:
"""Returns the `Interval's` upper bound."""
return self._upper
@property
def environment(self) -> "Environment":
"""Returns the `Interval's` `Environment`."""
return self._lower.environment
[docs]
def is_left_open(self) -> bool:
"""Returns `True` if the `lower` bound of this `Interval` is not included in the `Interval`, `False` otherwise."""
return self._is_left_open
[docs]
def is_right_open(self) -> bool:
"""Returns `True` if the `upper` bound of this `Interval` is not included in the `Interval`, `False` otherwise."""
return self._is_right_open
[docs]
class Duration(ABC):
pass
[docs]
class DurationInterval(Duration, Interval):
"""Class used to indicate that an `Interval` is also a `Duration`."""
def __init__(
self,
lower: FNode,
upper: FNode,
is_left_open: bool = False,
is_right_open: bool = False,
):
Duration.__init__(self)
Interval.__init__(self, lower, upper, is_left_open, is_right_open)
def __eq__(self, oth: object) -> bool:
if isinstance(oth, DurationInterval):
return (
self._lower == oth._lower
and self._upper == oth._upper
and self._is_left_open == oth._is_left_open
and self._is_right_open == oth._is_right_open
)
else:
return False
def __hash__(self) -> int:
return hash((self._lower, self.upper, self._is_left_open, self._is_right_open))
def ClosedDurationInterval(lower: FNode, upper: FNode) -> DurationInterval:
"""
Represents the (closed) interval duration constraint:
`[lower, upper]`
:param lower: The expression defining the `lower` bound of this interval.
:param upper: The expression defining the `upper` bound of this interval.
:return: The created `DurationInterval`.
"""
return DurationInterval(lower, upper)
def FixedDuration(size: FNode) -> DurationInterval:
"""
Represents a fixed duration constraint.
:param size: The expression defining the only value in this `interval`.
:return: The created `DurationInterval`.
"""
return DurationInterval(size, size)
def OpenDurationInterval(lower: FNode, upper: FNode) -> DurationInterval:
"""Represents the (open) interval duration constraint:
`(lower, upper)`
:param lower: The expression defining the `lower` bound of this interval.
:param upper: The expression defining the `upper` bound of this interval.
:return: The created `DurationInterval`.
"""
return DurationInterval(lower, upper, True, True)
def LeftOpenDurationInterval(lower: FNode, upper: FNode) -> DurationInterval:
"""Represents the (left open, right closed) interval duration constraint:
`(lower, upper]`
:param lower: The expression defining the `lower` bound of this interval.
:param upper: The expression defining the `upper` bound of this interval.
:return: The created `DurationInterval`.
"""
return DurationInterval(lower, upper, True, False)
def RightOpenDurationInterval(lower: FNode, upper: FNode) -> DurationInterval:
"""
Represents the (left closed, right open) interval duration constraint:
`[lower, upper)`
:param lower: The expression defining the `lower` bound of this interval.
:param upper: The expression defining the `upper` bound of this interval.
:return: The created `DurationInterval`.
"""
return DurationInterval(lower, upper, False, True)
class TimeInterval:
"""Represents an `Interval` where the 2 bounds are :class:`~unified_planning.model.Timing`."""
def __init__(
self,
lower: Timing,
upper: Timing,
is_left_open: bool = False,
is_right_open: bool = False,
):
self._lower = lower
self._upper = upper
self._is_left_open = is_left_open
self._is_right_open = is_right_open
def __repr__(self) -> str:
if self.is_left_open():
left_bound = "("
else:
left_bound = "["
if self.is_right_open():
right_bound = ")"
else:
right_bound = "]"
if self.lower == self.upper:
return f"{left_bound}{str(self.lower)}{right_bound}"
else:
return f"{left_bound}{str(self.lower)}, {str(self.upper)}{right_bound}"
def __eq__(self, oth: object) -> bool:
if isinstance(oth, TimeInterval):
return (
self._lower == oth._lower
and self._upper == oth._upper
and self._is_left_open == oth._is_left_open
and self._is_right_open == oth._is_right_open
)
else:
return False
def __hash__(self) -> int:
res = hash(self._lower) + hash(self._upper)
if self._is_left_open:
res ^= hash("is_left_open")
if self._is_right_open:
res ^= hash("is_right_open")
return res
@property
def lower(self) -> Timing:
"""Returns the `TimeInterval's` lower bound."""
return self._lower
@property
def upper(self) -> Timing:
"""Returns the `TimeInterval's` upper bound."""
return self._upper
def is_left_open(self) -> bool:
"""Returns `False` if this `TimeInterval` lower bound is included in the Interval, `True` otherwise."""
return self._is_left_open
def is_right_open(self) -> bool:
"""Returns `False` if this `TimeInterval` upper bound is included in the Interval, `True` otherwise."""
return self._is_right_open
def TimePointInterval(tp: Timing) -> TimeInterval:
"""
Returns the (point) temporal interval: `[tp, tp]`
:param tp: The only `Timing` belonging to this interval.
:return: The created `TimeInterval`.
"""
return TimeInterval(tp, tp)
def ClosedTimeInterval(lower: Timing, upper: Timing) -> TimeInterval:
"""
Returns the (closed) temporal interval: `[lower, upper]`
:param lower: The `Timing` defining the `lower` bound of this interval.
:param upper: The `Timing` defining the `upper` bound of this interval.
:return: The created `TimeInterval`.
"""
return TimeInterval(lower, upper)
def OpenTimeInterval(lower: Timing, upper: Timing) -> TimeInterval:
"""
Returns the (open) temporal interval: `(lower, upper)`
:param lower: The `Timing` defining the `lower` bound of this interval.
:param upper: The `Timing` defining the `upper` bound of this interval.
:return: The created `TimeInterval`.
"""
return TimeInterval(lower, upper, True, True)
def LeftOpenTimeInterval(lower: Timing, upper: Timing) -> TimeInterval:
"""
Returns the (left open, right closed) temporal interval: `(lower, upper]`
:param lower: The `Timing` defining the `lower` bound of this interval.
:param upper: The `Timing` defining the `upper` bound of this interval.
:return: The created `TimeInterval`.
"""
return TimeInterval(lower, upper, True, False)
def RightOpenTimeInterval(lower: Timing, upper: Timing) -> TimeInterval:
"""
Returns the (left closed, right open) temporal interval: `[lower, upper)`
:param lower: The `Timing` defining the `lower` bound of this interval.
:param upper: The `Timing` defining the `upper` bound of this interval.
:return: The created `TimeInterval`.
"""
return TimeInterval(lower, upper, False, True)