Source code for unified_planning.engines.results

# Copyright 2021 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 the PlanGenerationResult class."""


import unified_planning as up
from unified_planning.exceptions import UPUsageError
from unified_planning.model import AbstractProblem
from unified_planning.plans import ActionInstance
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Callable, Dict, Optional, List


class ValidationResultStatus(Enum):
    """
    Enum representing the 2 possible values in the `status` field of a :class:`~unified_planning.engines.ValidationResult`:
    VALID or INVALID.
    """

    VALID = (
        auto()
    )  # The plan is valid for the problem, it satisfies all the hard constraints
    INVALID = (
        auto()
    )  # The plan is invalid for the problem, it does not satisfy all the hard constraints


[docs]class PlanGenerationResultStatus(Enum): """ Enum representing the 9 possible values in the status field of a :class:`~unified_planning.engines.PlanGenerationResult`: SOLVED_SATISFICING -> Valid plan found. SOLVED_OPTIMALLY -> Optimal plan found. UNSOLVABLE_PROVEN -> The problem is impossible, no valid plan exists. UNSOLVABLE_INCOMPLETELY -> The planner could not find a plan, but it's not sure that the problem is impossible (The planner is incomplete) TIMEOUT -> The planner ran out of time MEMOUT -> The planner ran out of memory INTERNAL_ERROR -> The planner had an internal error UNSUPPORTED_PROBLEM -> The problem given is not supported by the planner INTERMEDIATE -> The report is not a final one but it's given through the callback function """ SOLVED_SATISFICING = auto() # Valid plan found. SOLVED_OPTIMALLY = auto() # Optimal plan found. UNSOLVABLE_PROVEN = auto() # The problem is impossible, no valid plan exists. UNSOLVABLE_INCOMPLETELY = ( auto() ) # The planner could not find a plan, but it's not sure that the problem is impossible (The planner is incomplete) TIMEOUT = auto() # The planner ran out of time MEMOUT = auto() # The planner ran out of memory INTERNAL_ERROR = auto() # The planner had an internal error UNSUPPORTED_PROBLEM = auto() # The problem given is not supported by the planner INTERMEDIATE = ( auto() ) # The report is not a final one but it's given through the callback function
POSITIVE_OUTCOMES = frozenset( [ PlanGenerationResultStatus.SOLVED_SATISFICING, PlanGenerationResultStatus.SOLVED_OPTIMALLY, ] ) NEGATIVE_OUTCOMES = frozenset( [ PlanGenerationResultStatus.UNSOLVABLE_PROVEN, PlanGenerationResultStatus.UNSOLVABLE_INCOMPLETELY, PlanGenerationResultStatus.UNSUPPORTED_PROBLEM, ] ) class LogLevel(Enum): """ Enum representing the 4 possible values in the verbosity level of a :class:`~unified_planning.engines.LogMessage`: DEBUG, INFO, WARNING and ERROR """ DEBUG = auto() INFO = auto() WARNING = auto() ERROR = auto() @dataclass class LogMessage: """ This class is composed by a message and the Enum LogLevel indicating this message level, like Debug, Info, Warning or Error. """ level: LogLevel message: str @dataclass class Result: """This class represents the base class for results given by the engines to the user.""" def is_definitive_result(self, *args) -> bool: """This predicate should state if the Result is definitive or if it can be improved.""" raise NotImplementedError @dataclass class PlanGenerationResult(Result): """Class that represents the result of a plan generation call.""" status: PlanGenerationResultStatus plan: Optional["up.plans.Plan"] engine_name: str metrics: Optional[Dict[str, str]] = field(default=None) log_messages: Optional[List[LogMessage]] = field(default=None) def __post__init(self): # Checks that plan and status are consistent if self.status in POSITIVE_OUTCOMES and self.plan is None: raise UPUsageError( f"The Result status is {str(self.status)} but no plan is set." ) elif self.status in NEGATIVE_OUTCOMES and self.plan is not None: raise UPUsageError( f"The Result status is {str(self.status)} but the plan is {str(self.plan)}.\nWith this status the plan must be None." ) return self def is_definitive_result(self, *args) -> bool: optimality_required = False if len(args) > 0: optimality_required = ( len(args[0].quality_metrics) > 0 ) # Require optimality if the problem has at least one quality metric. return ( self.status == PlanGenerationResultStatus.SOLVED_OPTIMALLY or self.status == PlanGenerationResultStatus.UNSOLVABLE_PROVEN or ( optimality_required and self.status == PlanGenerationResultStatus.SOLVED_SATISFICING ) ) @dataclass class ValidationResult(Result): """Class that represents the result of a validate call.""" status: ValidationResultStatus engine_name: str log_messages: Optional[List[LogMessage]] = field(default=None) def is_definitive_result(self, *args) -> bool: return True @dataclass class CompilerResult(Result): """Class that represents the result of a compile call.""" problem: Optional[AbstractProblem] map_back_action_instance: Optional[ Callable[[ActionInstance], Optional[ActionInstance]] ] engine_name: str log_messages: Optional[List[LogMessage]] = field(default=None) def _post_init(self): # Check that compiled problem and map_back_action_instance are consistent with each other if self.problem is None and self.map_back_action_instance is not None: raise UPUsageError( f"The compiled Problem is None but the map_back_action_instance Callable is not None." ) if self.problem is not None and self.map_back_action_instance is None: raise UPUsageError( f"The compiled Problem is {str(self.problem)} but the map_back_action_instance Callable is None." ) def is_definitive_result(self, *args) -> bool: return self.problem is not None