From 2135f274092258706f362be47f9bd6e51b0a1b77 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 29 Nov 2024 11:23:36 +0100 Subject: [PATCH 01/33] separating events and processes from actions - work in progress --- unified_planning/model/transition.py | 696 +++++++++++++++++++++++++++ 1 file changed, 696 insertions(+) create mode 100644 unified_planning/model/transition.py diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py new file mode 100644 index 000000000..943f225a9 --- /dev/null +++ b/unified_planning/model/transition.py @@ -0,0 +1,696 @@ +# 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 the `Action` base class and some of his extensions. +An `Action` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` +and a `list` of `effects`. +""" + + +import unified_planning as up +from unified_planning.environment import get_environment, Environment +from unified_planning.exceptions import ( + UPTypeError, + UPUnboundedVariablesError, + UPProblemDefinitionError, + UPUsageError, +) +from unified_planning.model.mixins.timed_conds_effs import TimedCondsEffs +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Set, Union, Optional, Iterable +from collections import OrderedDict + + +class Transition(ABC): + """This is the `Action` interface.""" + + def __init__( + self, + _name: str, + _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, + _env: Optional[Environment] = None, + **kwargs: "up.model.types.Type", + ): + self._environment = get_environment(_env) + self._name = _name + self._parameters: "OrderedDict[str, up.model.parameter.Parameter]" = ( + OrderedDict() + ) + if _parameters is not None: + assert len(kwargs) == 0 + for n, t in _parameters.items(): + assert self._environment.type_manager.has_type( + t + ), "type of parameter does not belong to the same environment of the action" + self._parameters[n] = up.model.parameter.Parameter( + n, t, self._environment + ) + else: + for n, t in kwargs.items(): + assert self._environment.type_manager.has_type( + t + ), "type of parameter does not belong to the same environment of the action" + self._parameters[n] = up.model.parameter.Parameter( + n, t, self._environment + ) + + @abstractmethod + def __eq__(self, oth: object) -> bool: + raise NotImplementedError + + def _print_parameters(self, s): + first = True + for p in self.parameters: + if first: + s.append("(") + first = False + else: + s.append(", ") + s.append(str(p)) + if not first: + s.append(")") + + @abstractmethod + def __hash__(self) -> int: + raise NotImplementedError + + def __call__( + self, + *args: "up.model.Expression", + agent: Optional["up.model.multi_agent.Agent"] = None, + motion_paths: Optional[ + Dict["up.model.tamp.MotionConstraint", "up.model.tamp.Path"] + ] = None, + ) -> "up.plans.plan.ActionInstance": + params = tuple(args) + return up.plans.plan.ActionInstance( + self, params, agent=agent, motion_paths=motion_paths + ) + + @abstractmethod + def clone(self): + raise NotImplementedError + + @property + def environment(self) -> Environment: + """Returns this `Action` `Environment`.""" + return self._environment + + @property + def name(self) -> str: + """Returns the `Action` `name`.""" + return self._name + + @name.setter + def name(self, new_name: str): + """Sets the `Action` `name`.""" + self._name = new_name + + @property + def parameters(self) -> List["up.model.parameter.Parameter"]: + """Returns the `list` of the `Action parameters`.""" + return list(self._parameters.values()) + + def parameter(self, name: str) -> "up.model.parameter.Parameter": + """ + Returns the `parameter` of the `Action` with the given `name`. + + Example + ------- + >>> from unified_planning.shortcuts import * + >>> location_type = UserType("Location") + >>> move = InstantaneousAction("move", source=location_type, target=location_type) + >>> move.parameter("source") # return the "source" parameter of the action, with type "Location" + Location source + >>> move.parameter("target") + Location target + + If a parameter's name (1) does not conflict with an existing attribute of `Action` and (2) does not start with '_' + it can also be accessed as if it was an attribute of the action. For instance: + + >>> move.source + Location source + + :param name: The `name` of the target `parameter`. + :return: The `parameter` of the `Action` with the given `name`. + """ + if name not in self._parameters: + raise ValueError(f"Action '{self.name}' has no parameter '{name}'") + return self._parameters[name] + + def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": + if parameter_name.startswith("_"): + # guard access as pickling relies on attribute error to be thrown even when + # no attributes of the object have been set. + # In this case accessing `self._name` or `self._parameters`, would re-invoke __getattr__ + raise AttributeError(f"Action has no attribute '{parameter_name}'") + if parameter_name not in self._parameters: + raise AttributeError( + f"Action '{self.name}' has no attribute or parameter '{parameter_name}'" + ) + return self._parameters[parameter_name] + + def is_conditional(self) -> bool: + """Returns `True` if the `Action` has `conditional effects`, `False` otherwise.""" + raise NotImplementedError + + +""" +Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: +Processes or Events. +Processes dictate how numeric variables evolve over time through the use of time-derivative functions +Events dictate the analogous of urgent transitions in timed automata theory +""" + + +class Process(Transition): + """This is the `Process` class, which implements the abstract `Action` class.""" + + def __init__( + self, + _name: str, + _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, + _env: Optional[Environment] = None, + **kwargs: "up.model.types.Type", + ): + Transition.__init__(self, _name, _parameters, _env, **kwargs) + self._preconditions: List["up.model.fnode.FNode"] = [] + self._effects: List[up.model.effect.Effect] = [] + self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None + # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment + self._fluents_assigned: Dict[ + "up.model.fnode.FNode", "up.model.fnode.FNode" + ] = {} + # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease + self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + + def __repr__(self) -> str: + s = [] + s.append(f"process {self.name}") + self._print_parameters(s) + s.append(" {\n") + s.append(" preconditions = [\n") + for c in self.preconditions: + s.append(f" {str(c)}\n") + s.append(" ]\n") + s.append(" effects = [\n") + for e in self.effects: + s.append(f" {str(e)}\n") + s.append(" ]\n") + s.append(" }") + return "".join(s) + + def __eq__(self, oth: object) -> bool: + if isinstance(oth, Process): + cond = ( + self._environment == oth._environment + and self._name == oth._name + and self._parameters == oth._parameters + ) + return ( + cond + and set(self._preconditions) == set(oth._preconditions) + and set(self._effects) == set(oth._effects) + and self._simulated_effect == oth._simulated_effect + ) + else: + return False + + def __hash__(self) -> int: + res = hash(self._name) + for ap in self._parameters.items(): + res += hash(ap) + for p in self._preconditions: + res += hash(p) + for e in self._effects: + res += hash(e) + res += hash(self._simulated_effect) + return res + + def clone(self): + new_params = OrderedDict( + (param_name, param.type) for param_name, param in self._parameters.items() + ) + new_process = Process(self._name, new_params, self._environment) + new_process._preconditions = self._preconditions[:] + new_process._effects = [e.clone() for e in self._effects] + new_process._fluents_assigned = self._fluents_assigned.copy() + new_process._fluents_inc_dec = self._fluents_inc_dec.copy() + new_process._simulated_effect = self._simulated_effect + return new_process + + @property + def preconditions(self) -> List["up.model.fnode.FNode"]: + """Returns the `list` of the `Process` `preconditions`.""" + return self._preconditions + + def clear_preconditions(self): + """Removes all the `Process preconditions`""" + self._preconditions = [] + + @property + def effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `Process effects`.""" + return self._effects + + def clear_effects(self): + """Removes all the `Process's effects`.""" + self._effects = [] + self._fluents_assigned = {} + self._fluents_inc_dec = set() + + def _add_effect_instance(self, effect: "up.model.effect.Effect"): + assert ( + effect.environment == self._environment + ), "effect does not have the same environment of the Process" + + self._effects.append(effect) + + def add_precondition( + self, + precondition: Union[ + "up.model.fnode.FNode", + "up.model.fluent.Fluent", + "up.model.parameter.Parameter", + bool, + ], + ): + """ + Adds the given expression to `Process's preconditions`. + + :param precondition: The expression that must be added to the `Process's preconditions`. + """ + (precondition_exp,) = self._environment.expression_manager.auto_promote( + precondition + ) + assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() + if precondition_exp == self._environment.expression_manager.TRUE(): + return + free_vars = self._environment.free_vars_oracle.get_free_variables( + precondition_exp + ) + if len(free_vars) != 0: + raise UPUnboundedVariablesError( + f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" + ) + if precondition_exp not in self._preconditions: + self._preconditions.append(precondition_exp) + + def add_derivative( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + ): + """ + Adds the given `time derivative effect` to the `process's effects`. + + :param fluent: The `fluent` is the numeric state variable of which this process expresses its time derivative, which in Newton's notation would be over-dot(fluent). + :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote( + fluent, + value, + True, + ) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." + ) + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"Process effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Derivative can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.DERIVATIVE, + forall=tuple(), + ) + ) + + +class Event(Transition): + """This class represents an event.""" + + def __init__( + self, + _name: str, + _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, + _env: Optional[Environment] = None, + **kwargs: "up.model.types.Type", + ): + Transition.__init__(self, _name, _parameters, _env, **kwargs) + self._preconditions: List["up.model.fnode.FNode"] = [] + self._effects: List[up.model.effect.Effect] = [] + self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None + # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment + self._fluents_assigned: Dict[ + "up.model.fnode.FNode", "up.model.fnode.FNode" + ] = {} + # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease + self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + + def __eq__(self, oth: object) -> bool: + if isinstance(oth, Event): + cond = ( + self._environment == oth._environment + and self._name == oth._name + and self._parameters == oth._parameters + ) + return ( + cond + and set(self._preconditions) == set(oth._preconditions) + and set(self._effects) == set(oth._effects) + and self._simulated_effect == oth._simulated_effect + ) + else: + return False + + def __hash__(self) -> int: + res = hash(self._name) + for ap in self._parameters.items(): + res += hash(ap) + for p in self._preconditions: + res += hash(p) + for e in self._effects: + res += hash(e) + res += hash(self._simulated_effect) + return res + + def clone(self): + new_params = OrderedDict( + (param_name, param.type) for param_name, param in self._parameters.items() + ) + new_instantaneous_action = Event( + self._name, new_params, self._environment + ) + new_instantaneous_action._preconditions = self._preconditions[:] + new_instantaneous_action._effects = [e.clone() for e in self._effects] + new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() + new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() + new_instantaneous_action._simulated_effect = self._simulated_effect + return new_instantaneous_action + + @property + def preconditions(self) -> List["up.model.fnode.FNode"]: + """Returns the `list` of the `Action` `preconditions`.""" + return self._preconditions + + def clear_preconditions(self): + """Removes all the `Action preconditions`""" + self._preconditions = [] + + @property + def effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `Action effects`.""" + return self._effects + + def clear_effects(self): + """Removes all the `Action's effects`.""" + self._effects = [] + self._fluents_assigned = {} + self._fluents_inc_dec = set() + self._simulated_effect = None + + @property + def conditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action conditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if e.is_conditional()] + + def is_conditional(self) -> bool: + """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" + return any(e.is_conditional() for e in self._effects) + + @property + def unconditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action unconditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if not e.is_conditional()] + + def add_precondition( + self, + precondition: Union[ + "up.model.fnode.FNode", + "up.model.fluent.Fluent", + "up.model.parameter.Parameter", + bool, + ], + ): + """ + Adds the given expression to `action's preconditions`. + + :param precondition: The expression that must be added to the `action's preconditions`. + """ + (precondition_exp,) = self._environment.expression_manager.auto_promote( + precondition + ) + assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() + if precondition_exp == self._environment.expression_manager.TRUE(): + return + free_vars = self._environment.free_vars_oracle.get_free_variables( + precondition_exp + ) + if len(free_vars) != 0: + raise UPUnboundedVariablesError( + f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" + ) + if precondition_exp not in self._preconditions: + self._preconditions.append(precondition_exp) + + def add_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `assignment` to the `action's effects`. + + :param fluent: The `fluent` of which `value` is modified by the `assignment`. + :param value: The `value` to assign to the given `fluent`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." + ) + if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + # Value is not assignable to fluent (its type is not a subset of the fluent's type). + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + self._add_effect_instance( + up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) + ) + + def add_increase_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `increase effect` to the `action's effects`. + + :param fluent: The `fluent` which `value` is increased. + :param value: The given `fluent` is incremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote( + fluent, + value, + condition, + ) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Increase effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.INCREASE, + forall=forall, + ) + ) + + def add_decrease_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `decrease effect` to the `action's effects`. + + :param fluent: The `fluent` which value is decreased. + :param value: The given `fluent` is decremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Decrease effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.DECREASE, + forall=forall, + ) + ) + + def _add_effect_instance(self, effect: "up.model.effect.Effect"): + assert ( + effect.environment == self._environment + ), "effect does not have the same environment of the action" + up.model.effect.check_conflicting_effects( + effect, + None, + self._simulated_effect, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + self._effects.append(effect) + + @property + def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: + """Returns the `action` `simulated effect`.""" + return self._simulated_effect + + def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): + """ + Sets the given `simulated effect` as the only `action's simulated effect`. + + :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only + `simulated effect`. + """ + up.model.effect.check_conflicting_simulated_effects( + simulated_effect, + None, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + if simulated_effect.environment != self.environment: + raise UPUsageError( + "The added SimulatedEffect does not have the same environment of the Action" + ) + self._simulated_effect = simulated_effect + + def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): + self._preconditions = preconditions + + + def __hash__(self) -> int: + res = hash(self._name) + for ap in self._parameters.items(): + res += hash(ap) + for p in self._preconditions: + res += hash(p) + for e in self._effects: + res += hash(e) + res += hash(self._simulated_effect) + return res + + def __repr__(self) -> str: + s = [] + s.append(f"event {self.name}") + self._print_parameters(s) + s.append(" {\n") + s.append(" preconditions = [\n") + for c in self.preconditions: + s.append(f" {str(c)}\n") + s.append(" ]\n") + s.append(" effects = [\n") + for e in self.effects: + s.append(f" {str(e)}\n") + s.append(" ]\n") + if self._simulated_effect is not None: + s.append(f" simulated effect = {self._simulated_effect}\n") + s.append(" }") + return "".join(s) + + def clone(self): + new_params = OrderedDict( + (param_name, param.type) for param_name, param in self._parameters.items() + ) + new_instantaneous_action = Event(self._name, new_params, self._environment) + new_instantaneous_action._preconditions = self._preconditions[:] + new_instantaneous_action._effects = [e.clone() for e in self._effects] + new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() + new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() + new_instantaneous_action._simulated_effect = self._simulated_effect + return new_instantaneous_action From fcbf8946df9fadeefedfbc51d4a783ae1d29bfbd Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 29 Nov 2024 11:34:01 +0100 Subject: [PATCH 02/33] separating events and processes from actions - work in progress --- unified_planning/model/__init__.py | 5 +- unified_planning/model/action.py | 445 ++----------------- unified_planning/model/mixins/actions_set.py | 8 +- unified_planning/model/problem.py | 24 +- unified_planning/test/examples/processes.py | 2 +- unified_planning/test/test_model.py | 8 +- unified_planning/test/test_pddl_io.py | 2 +- 7 files changed, 69 insertions(+), 425 deletions(-) diff --git a/unified_planning/model/__init__.py b/unified_planning/model/__init__.py index d0f61f013..abc20cd1d 100644 --- a/unified_planning/model/__init__.py +++ b/unified_planning/model/__init__.py @@ -19,8 +19,11 @@ InstantaneousAction, DurativeAction, SensingAction, +) + +from unified_planning.model.transition import( Process, - Event, + Event, ) from unified_planning.model.effect import Effect, SimulatedEffect, EffectKind from unified_planning.model.expression import ( diff --git a/unified_planning/model/action.py b/unified_planning/model/action.py index 47b25bce1..1366cf60d 100644 --- a/unified_planning/model/action.py +++ b/unified_planning/model/action.py @@ -32,142 +32,14 @@ from typing import Any, Dict, List, Set, Union, Optional, Iterable from collections import OrderedDict +from unified_planning.model.transition import Transition -class Action(ABC): - """This is the `Action` interface.""" - - def __init__( - self, - _name: str, - _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, - _env: Optional[Environment] = None, - **kwargs: "up.model.types.Type", - ): - self._environment = get_environment(_env) - self._name = _name - self._parameters: "OrderedDict[str, up.model.parameter.Parameter]" = ( - OrderedDict() - ) - if _parameters is not None: - assert len(kwargs) == 0 - for n, t in _parameters.items(): - assert self._environment.type_manager.has_type( - t - ), "type of parameter does not belong to the same environment of the action" - self._parameters[n] = up.model.parameter.Parameter( - n, t, self._environment - ) - else: - for n, t in kwargs.items(): - assert self._environment.type_manager.has_type( - t - ), "type of parameter does not belong to the same environment of the action" - self._parameters[n] = up.model.parameter.Parameter( - n, t, self._environment - ) - - @abstractmethod - def __eq__(self, oth: object) -> bool: - raise NotImplementedError - - def _print_parameters(self, s): - first = True - for p in self.parameters: - if first: - s.append("(") - first = False - else: - s.append(", ") - s.append(str(p)) - if not first: - s.append(")") - - @abstractmethod - def __hash__(self) -> int: - raise NotImplementedError - - def __call__( - self, - *args: "up.model.Expression", - agent: Optional["up.model.multi_agent.Agent"] = None, - motion_paths: Optional[ - Dict["up.model.tamp.MotionConstraint", "up.model.tamp.Path"] - ] = None, - ) -> "up.plans.plan.ActionInstance": - params = tuple(args) - return up.plans.plan.ActionInstance( - self, params, agent=agent, motion_paths=motion_paths - ) - - @abstractmethod - def clone(self): - raise NotImplementedError - - @property - def environment(self) -> Environment: - """Returns this `Action` `Environment`.""" - return self._environment - - @property - def name(self) -> str: - """Returns the `Action` `name`.""" - return self._name - - @name.setter - def name(self, new_name: str): - """Sets the `Action` `name`.""" - self._name = new_name - - @property - def parameters(self) -> List["up.model.parameter.Parameter"]: - """Returns the `list` of the `Action parameters`.""" - return list(self._parameters.values()) - - def parameter(self, name: str) -> "up.model.parameter.Parameter": - """ - Returns the `parameter` of the `Action` with the given `name`. - - Example - ------- - >>> from unified_planning.shortcuts import * - >>> location_type = UserType("Location") - >>> move = InstantaneousAction("move", source=location_type, target=location_type) - >>> move.parameter("source") # return the "source" parameter of the action, with type "Location" - Location source - >>> move.parameter("target") - Location target - - If a parameter's name (1) does not conflict with an existing attribute of `Action` and (2) does not start with '_' - it can also be accessed as if it was an attribute of the action. For instance: - - >>> move.source - Location source - - :param name: The `name` of the target `parameter`. - :return: The `parameter` of the `Action` with the given `name`. - """ - if name not in self._parameters: - raise ValueError(f"Action '{self.name}' has no parameter '{name}'") - return self._parameters[name] - - def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": - if parameter_name.startswith("_"): - # guard access as pickling relies on attribute error to be thrown even when - # no attributes of the object have been set. - # In this case accessing `self._name` or `self._parameters`, would re-invoke __getattr__ - raise AttributeError(f"Action has no attribute '{parameter_name}'") - if parameter_name not in self._parameters: - raise AttributeError( - f"Action '{self.name}' has no attribute or parameter '{parameter_name}'" - ) - return self._parameters[parameter_name] - def is_conditional(self) -> bool: - """Returns `True` if the `Action` has `conditional effects`, `False` otherwise.""" - raise NotImplementedError +class Action(Transition): + """This is the `Action` interface.""" -class InstantaneousTransitionMixin(Action): +class InstantaneousAction(Action): """Represents an instantaneous action.""" def __init__( @@ -188,8 +60,35 @@ def __init__( # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + def __repr__(self) -> str: + s = [] + s.append(f"action {self.name}") + first = True + for p in self.parameters: + if first: + s.append("(") + first = False + else: + s.append(", ") + s.append(str(p)) + if not first: + s.append(")") + s.append(" {\n") + s.append(" preconditions = [\n") + for c in self.preconditions: + s.append(f" {str(c)}\n") + s.append(" ]\n") + s.append(" effects = [\n") + for e in self.effects: + s.append(f" {str(e)}\n") + s.append(" ]\n") + if self._simulated_effect is not None: + s.append(f" simulated effect = {self._simulated_effect}\n") + s.append(" }") + return "".join(s) + def __eq__(self, oth: object) -> bool: - if isinstance(oth, InstantaneousTransitionMixin): + if isinstance(oth, InstantaneousAction): cond = ( self._environment == oth._environment and self._name == oth._name @@ -219,7 +118,7 @@ def clone(self): new_params = OrderedDict( (param_name, param.type) for param_name, param in self._parameters.items() ) - new_instantaneous_action = InstantaneousTransitionMixin( + new_instantaneous_action = InstantaneousAction( self._name, new_params, self._environment ) new_instantaneous_action._preconditions = self._preconditions[:] @@ -472,40 +371,6 @@ def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): self._preconditions = preconditions -class InstantaneousAction(InstantaneousTransitionMixin): - def __repr__(self) -> str: - s = [] - s.append(f"action {self.name}") - self._print_parameters(s) - s.append(" {\n") - s.append(" preconditions = [\n") - for c in self.preconditions: - s.append(f" {str(c)}\n") - s.append(" ]\n") - s.append(" effects = [\n") - for e in self.effects: - s.append(f" {str(e)}\n") - s.append(" ]\n") - if self._simulated_effect is not None: - s.append(f" simulated effect = {self._simulated_effect}\n") - s.append(" }") - return "".join(s) - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_instantaneous_action = InstantaneousAction( - self._name, new_params, self._environment - ) - new_instantaneous_action._preconditions = self._preconditions[:] - new_instantaneous_action._effects = [e.clone() for e in self._effects] - new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() - new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() - new_instantaneous_action._simulated_effect = self._simulated_effect - return new_instantaneous_action - - class DurativeAction(Action, TimedCondsEffs): """Represents a durative action.""" @@ -782,246 +647,4 @@ def __repr__(self) -> str: s.append(f" {str(e)}\n") s.append(" ]\n") s.append(" }") - return "".join(s) - - -""" -Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: -Processes or Events. -Processes dictate how numeric variables evolve over time through the use of time-derivative functions -Events dictate the analogous of urgent transitions in timed automata theory -""" - - -class Process(Action): - """This is the `Process` class, which implements the abstract `Action` class.""" - - def __init__( - self, - _name: str, - _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, - _env: Optional[Environment] = None, - **kwargs: "up.model.types.Type", - ): - Action.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] - self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None - # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment - self._fluents_assigned: Dict[ - "up.model.fnode.FNode", "up.model.fnode.FNode" - ] = {} - # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease - self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() - - def __repr__(self) -> str: - s = [] - s.append(f"process {self.name}") - self._print_parameters(s) - s.append(" {\n") - s.append(" preconditions = [\n") - for c in self.preconditions: - s.append(f" {str(c)}\n") - s.append(" ]\n") - s.append(" effects = [\n") - for e in self.effects: - s.append(f" {str(e)}\n") - s.append(" ]\n") - s.append(" }") - return "".join(s) - - def __eq__(self, oth: object) -> bool: - if isinstance(oth, Process): - cond = ( - self._environment == oth._environment - and self._name == oth._name - and self._parameters == oth._parameters - ) - return ( - cond - and set(self._preconditions) == set(oth._preconditions) - and set(self._effects) == set(oth._effects) - and self._simulated_effect == oth._simulated_effect - ) - else: - return False - - def __hash__(self) -> int: - res = hash(self._name) - for ap in self._parameters.items(): - res += hash(ap) - for p in self._preconditions: - res += hash(p) - for e in self._effects: - res += hash(e) - res += hash(self._simulated_effect) - return res - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_process = Process(self._name, new_params, self._environment) - new_process._preconditions = self._preconditions[:] - new_process._effects = [e.clone() for e in self._effects] - new_process._fluents_assigned = self._fluents_assigned.copy() - new_process._fluents_inc_dec = self._fluents_inc_dec.copy() - new_process._simulated_effect = self._simulated_effect - return new_process - - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Process` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Process preconditions`""" - self._preconditions = [] - - @property - def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Process effects`.""" - return self._effects - - def clear_effects(self): - """Removes all the `Process's effects`.""" - self._effects = [] - self._fluents_assigned = {} - self._fluents_inc_dec = set() - - def _add_effect_instance(self, effect: "up.model.effect.Effect"): - assert ( - effect.environment == self._environment - ), "effect does not have the same environment of the Process" - - self._effects.append(effect) - - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `Process's preconditions`. - - :param precondition: The expression that must be added to the `Process's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - - def add_derivative( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - ): - """ - Adds the given `time derivative effect` to the `process's effects`. - - :param fluent: The `fluent` is the numeric state variable of which this process expresses its time derivative, which in Newton's notation would be over-dot(fluent). - :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote( - fluent, - value, - True, - ) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." - ) - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"Process effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Derivative can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DERIVATIVE, - forall=tuple(), - ) - ) - - -class Event(InstantaneousTransitionMixin): - """This class represents an event.""" - - def __init__( - self, - _name: str, - _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, - _env: Optional[Environment] = None, - **kwargs: "up.model.types.Type", - ): - InstantaneousTransitionMixin.__init__(self, _name, _parameters, _env, **kwargs) - - def __eq__(self, oth: object) -> bool: - if isinstance(oth, Event): - return super().__eq__(oth) - else: - return False - - def __hash__(self) -> int: - res = hash(self._name) - for ap in self._parameters.items(): - res += hash(ap) - for p in self._preconditions: - res += hash(p) - for e in self._effects: - res += hash(e) - res += hash(self._simulated_effect) - return res - - def __repr__(self) -> str: - s = [] - s.append(f"event {self.name}") - self._print_parameters(s) - s.append(" {\n") - s.append(" preconditions = [\n") - for c in self.preconditions: - s.append(f" {str(c)}\n") - s.append(" ]\n") - s.append(" effects = [\n") - for e in self.effects: - s.append(f" {str(e)}\n") - s.append(" ]\n") - if self._simulated_effect is not None: - s.append(f" simulated effect = {self._simulated_effect}\n") - s.append(" }") - return "".join(s) - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_instantaneous_action = Event(self._name, new_params, self._environment) - new_instantaneous_action._preconditions = self._preconditions[:] - new_instantaneous_action._effects = [e.clone() for e in self._effects] - new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() - new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() - new_instantaneous_action._simulated_effect = self._simulated_effect - return new_instantaneous_action + return "".join(s) \ No newline at end of file diff --git a/unified_planning/model/mixins/actions_set.py b/unified_planning/model/mixins/actions_set.py index a2aeaabfb..80d7fdcb9 100644 --- a/unified_planning/model/mixins/actions_set.py +++ b/unified_planning/model/mixins/actions_set.py @@ -83,23 +83,23 @@ def durative_actions(self) -> Iterator["up.model.action.DurativeAction"]: yield a @property - def processes(self) -> Iterator["up.model.action.Process"]: + def processes(self) -> Iterator["up.model.transition.Process"]: """Returs all the sensing actions of the problem. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" for a in self._actions: - if isinstance(a, up.model.action.Process): + if isinstance(a, up.model.transition.Process): yield a @property - def events(self) -> Iterator["up.model.action.Event"]: + def events(self) -> Iterator["up.model.transition.Event"]: """Returs all the sensing actions of the problem. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" for a in self._actions: - if isinstance(a, up.model.action.Event): + if isinstance(a, up.model.transition.Event): yield a @property diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 4a1934ece..dc57c2102 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -311,7 +311,19 @@ def _get_static_and_unused_fluents( (f.fluent() for e in exps for f in fve.get(e)) ) for a in self._actions: - if isinstance(a, up.model.action.InstantaneousTransitionMixin): + if isinstance(a, up.model.action.InstantaneousAction): + remove_used_fluents(*a.preconditions) + for e in a.effects: + remove_used_fluents(e.fluent, e.value, e.condition) + static_fluents.discard(e.fluent.fluent()) + if a.simulated_effect is not None: + # empty the set because a simulated effect reads all the fluents + unused_fluents.clear() + for f in a.simulated_effect.fluents: + static_fluents.discard(f.fluent()) + + elif isinstance(a, up.model.transition.Event): + # NOTE copypaste of above, with mixin should become one single block remove_used_fluents(*a.preconditions) for e in a.effects: remove_used_fluents(e.fluent, e.value, e.condition) @@ -332,7 +344,7 @@ def _get_static_and_unused_fluents( unused_fluents.clear() for f in se.fluents: static_fluents.discard(f.fluent()) - elif isinstance(a, up.model.action.Process): + elif isinstance(a, up.model.transition.Process): for e in a.effects: remove_used_fluents(e.fluent, e.value, e.condition) static_fluents.discard(e.fluent.fluent()) @@ -992,7 +1004,7 @@ def update_problem_kind_action( if isinstance(action, up.model.tamp.InstantaneousMotionAction): if len(action.motion_constraints) > 0: self.kind.set_problem_class("TAMP") - if isinstance(action, up.model.action.InstantaneousTransitionMixin): + if isinstance(action, up.model.action.InstantaneousAction): for c in action.preconditions: self.update_problem_kind_expression(c) for e in action.effects: @@ -1010,8 +1022,10 @@ def update_problem_kind_action( if len(action.simulated_effects) > 0: self.kind.set_simulated_entities("SIMULATED_EFFECTS") self.kind.set_time("CONTINUOUS_TIME") - elif isinstance(action, up.model.action.Process): - pass + elif isinstance(action, up.model.transition.Process): + pass # TODO add Process kind + elif isinstance(action, up.model.transition.Event): + pass # TODO add Event kind else: raise NotImplementedError diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 999e45eb1..eaee2ffae 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -1,5 +1,5 @@ from unified_planning.shortcuts import * -from unified_planning.model.action import Process +from unified_planning.model.transition import Process from unified_planning.test import TestCase diff --git a/unified_planning/test/test_model.py b/unified_planning/test/test_model.py index dca8aa511..14ac7d60f 100644 --- a/unified_planning/test/test_model.py +++ b/unified_planning/test/test_model.py @@ -22,7 +22,7 @@ ) from unified_planning.test.examples import get_example_problems from unified_planning.test import unittest_TestCase, main -from unified_planning.model.action import InstantaneousTransitionMixin +from unified_planning.model.action import InstantaneousAction class TestModel(unittest_TestCase): @@ -71,7 +71,7 @@ def test_clone_problem_and_action(self): for action_1, action_2 in zip( problem_clone_1.actions, problem_clone_2.actions ): - if isinstance(action_2, InstantaneousTransitionMixin): + if isinstance(action_2, InstantaneousAction): action_2._effects = [] action_1_clone = action_1.clone() action_1_clone._effects = [] @@ -83,6 +83,10 @@ def test_clone_problem_and_action(self): action_2._effects = [] action_1_clone = action_1.clone() action_1_clone._effects = [] + elif isinstance(action_2, Event): + action_2._effects = [] + action_1_clone = action_1.clone() + action_1_clone._effects = [] else: raise NotImplementedError self.assertEqual(action_2, action_1_clone) diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index b39984fcd..a85bcdfe0 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -14,8 +14,8 @@ import os import tempfile +import pytest # type: ignore from typing import cast -import pytest import unified_planning from unified_planning.shortcuts import * from unified_planning.test import ( From f9300e32cf6ed26b3448349c0bee241f47f937d3 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 29 Nov 2024 11:43:21 +0100 Subject: [PATCH 03/33] black fix, some mypy fixes - work in progress --- unified_planning/model/__init__.py | 4 +- unified_planning/model/action.py | 15 +++++++- unified_planning/model/problem.py | 6 +-- unified_planning/model/transition.py | 53 ++++----------------------- unified_planning/test/test_pddl_io.py | 2 +- 5 files changed, 27 insertions(+), 53 deletions(-) diff --git a/unified_planning/model/__init__.py b/unified_planning/model/__init__.py index abc20cd1d..c2fd5d066 100644 --- a/unified_planning/model/__init__.py +++ b/unified_planning/model/__init__.py @@ -21,9 +21,9 @@ SensingAction, ) -from unified_planning.model.transition import( +from unified_planning.model.transition import ( Process, - Event, + Event, ) from unified_planning.model.effect import Effect, SimulatedEffect, EffectKind from unified_planning.model.expression import ( diff --git a/unified_planning/model/action.py b/unified_planning/model/action.py index 1366cf60d..a9c23c1ce 100644 --- a/unified_planning/model/action.py +++ b/unified_planning/model/action.py @@ -38,6 +38,19 @@ class Action(Transition): """This is the `Action` interface.""" + def __call__( + self, + *args: "up.model.Expression", + agent: Optional["up.model.multi_agent.Agent"] = None, + motion_paths: Optional[ + Dict["up.model.tamp.MotionConstraint", "up.model.tamp.Path"] + ] = None, + ) -> "up.plans.plan.ActionInstance": + params = tuple(args) + return up.plans.plan.ActionInstance( + self, params, agent=agent, motion_paths=motion_paths + ) + class InstantaneousAction(Action): """Represents an instantaneous action.""" @@ -647,4 +660,4 @@ def __repr__(self) -> str: s.append(f" {str(e)}\n") s.append(" ]\n") s.append(" }") - return "".join(s) \ No newline at end of file + return "".join(s) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index dc57c2102..78a778c60 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -322,7 +322,7 @@ def _get_static_and_unused_fluents( for f in a.simulated_effect.fluents: static_fluents.discard(f.fluent()) - elif isinstance(a, up.model.transition.Event): + elif isinstance(a, up.model.transition.Event): # NOTE copypaste of above, with mixin should become one single block remove_used_fluents(*a.preconditions) for e in a.effects: @@ -1023,9 +1023,9 @@ def update_problem_kind_action( self.kind.set_simulated_entities("SIMULATED_EFFECTS") self.kind.set_time("CONTINUOUS_TIME") elif isinstance(action, up.model.transition.Process): - pass # TODO add Process kind + pass # TODO add Process kind elif isinstance(action, up.model.transition.Event): - pass # TODO add Event kind + pass # TODO add Event kind else: raise NotImplementedError diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py index 943f225a9..cf9444f92 100644 --- a/unified_planning/model/transition.py +++ b/unified_planning/model/transition.py @@ -86,19 +86,6 @@ def _print_parameters(self, s): def __hash__(self) -> int: raise NotImplementedError - def __call__( - self, - *args: "up.model.Expression", - agent: Optional["up.model.multi_agent.Agent"] = None, - motion_paths: Optional[ - Dict["up.model.tamp.MotionConstraint", "up.model.tamp.Path"] - ] = None, - ) -> "up.plans.plan.ActionInstance": - params = tuple(args) - return up.plans.plan.ActionInstance( - self, params, agent=agent, motion_paths=motion_paths - ) - @abstractmethod def clone(self): raise NotImplementedError @@ -401,15 +388,13 @@ def clone(self): new_params = OrderedDict( (param_name, param.type) for param_name, param in self._parameters.items() ) - new_instantaneous_action = Event( - self._name, new_params, self._environment - ) - new_instantaneous_action._preconditions = self._preconditions[:] - new_instantaneous_action._effects = [e.clone() for e in self._effects] - new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() - new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() - new_instantaneous_action._simulated_effect = self._simulated_effect - return new_instantaneous_action + new_event = Event(self._name, new_params, self._environment) + new_event._preconditions = self._preconditions[:] + new_event._effects = [e.clone() for e in self._effects] + new_event._fluents_assigned = self._fluents_assigned.copy() + new_event._fluents_inc_dec = self._fluents_inc_dec.copy() + new_event._simulated_effect = self._simulated_effect + return new_event @property def preconditions(self) -> List["up.model.fnode.FNode"]: @@ -653,18 +638,6 @@ def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffec def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): self._preconditions = preconditions - - def __hash__(self) -> int: - res = hash(self._name) - for ap in self._parameters.items(): - res += hash(ap) - for p in self._preconditions: - res += hash(p) - for e in self._effects: - res += hash(e) - res += hash(self._simulated_effect) - return res - def __repr__(self) -> str: s = [] s.append(f"event {self.name}") @@ -682,15 +655,3 @@ def __repr__(self) -> str: s.append(f" simulated effect = {self._simulated_effect}\n") s.append(" }") return "".join(s) - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_instantaneous_action = Event(self._name, new_params, self._environment) - new_instantaneous_action._preconditions = self._preconditions[:] - new_instantaneous_action._effects = [e.clone() for e in self._effects] - new_instantaneous_action._fluents_assigned = self._fluents_assigned.copy() - new_instantaneous_action._fluents_inc_dec = self._fluents_inc_dec.copy() - new_instantaneous_action._simulated_effect = self._simulated_effect - return new_instantaneous_action diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index a85bcdfe0..198663512 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -14,7 +14,7 @@ import os import tempfile -import pytest # type: ignore +import pytest # type: ignore from typing import cast import unified_planning from unified_planning.shortcuts import * From 243902089454c60405d8439f87e011b0c3fd7562 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 29 Nov 2024 11:52:09 +0100 Subject: [PATCH 04/33] mypy fixes - work in progress --- unified_planning/io/pddl_reader.py | 2 +- unified_planning/model/__init__.py | 1 + unified_planning/model/mixins/actions_set.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index 745d8ce4f..761883374 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -413,7 +413,7 @@ def __init__(self, environment: typing.Optional[Environment] = None): def _parse_exp( self, problem: up.model.Problem, - act: typing.Optional[Union[up.model.Action, htn.Method, htn.TaskNetwork]], + act: typing.Optional[Union[up.model.Transition, htn.Method, htn.TaskNetwork]], types_map: TypesMap, var: Dict[str, up.model.Variable], exp: CustomParseResults, diff --git a/unified_planning/model/__init__.py b/unified_planning/model/__init__.py index c2fd5d066..d95d8d611 100644 --- a/unified_planning/model/__init__.py +++ b/unified_planning/model/__init__.py @@ -22,6 +22,7 @@ ) from unified_planning.model.transition import ( + Transition, Process, Event, ) diff --git a/unified_planning/model/mixins/actions_set.py b/unified_planning/model/mixins/actions_set.py index 80d7fdcb9..01b03ffaa 100644 --- a/unified_planning/model/mixins/actions_set.py +++ b/unified_planning/model/mixins/actions_set.py @@ -146,8 +146,8 @@ def has_action(self, name: str) -> bool: if a.name == name: return True return False - - def add_action(self, action: "up.model.action.Action"): +# TODO different add_action and add_*something_else* + def add_action(self, action: "up.model.transition.Transition"): """ Adds the given `action` to the `problem`. From 60756fc84fb5bc84ca96831135ef0a0a5ea30451 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 29 Nov 2024 11:52:23 +0100 Subject: [PATCH 05/33] mypy fixes - work in progress --- unified_planning/model/mixins/actions_set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unified_planning/model/mixins/actions_set.py b/unified_planning/model/mixins/actions_set.py index 01b03ffaa..a99f926bc 100644 --- a/unified_planning/model/mixins/actions_set.py +++ b/unified_planning/model/mixins/actions_set.py @@ -146,7 +146,8 @@ def has_action(self, name: str) -> bool: if a.name == name: return True return False -# TODO different add_action and add_*something_else* + + # TODO different add_action and add_*something_else* def add_action(self, action: "up.model.transition.Transition"): """ Adds the given `action` to the `problem`. From a0084442f804faeac7e63554ceff2c60efdac16d Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Mon, 2 Dec 2024 09:06:27 +0100 Subject: [PATCH 06/33] refactoring: added new file for natural transitions --- unified_planning/model/__init__.py | 8 +- unified_planning/model/mixins/actions_set.py | 8 +- unified_planning/model/natural_transition.py | 542 +++++++++++++++++++ unified_planning/model/problem.py | 8 +- unified_planning/model/transition.py | 503 ----------------- unified_planning/test/examples/processes.py | 2 +- 6 files changed, 557 insertions(+), 514 deletions(-) create mode 100644 unified_planning/model/natural_transition.py diff --git a/unified_planning/model/__init__.py b/unified_planning/model/__init__.py index d95d8d611..96d152094 100644 --- a/unified_planning/model/__init__.py +++ b/unified_planning/model/__init__.py @@ -21,11 +21,15 @@ SensingAction, ) -from unified_planning.model.transition import ( - Transition, +from unified_planning.model.natural_transition import ( + NaturalTransition, Process, Event, ) + +from unified_planning.model.transition import ( + Transition, +) from unified_planning.model.effect import Effect, SimulatedEffect, EffectKind from unified_planning.model.expression import ( BoolExpression, diff --git a/unified_planning/model/mixins/actions_set.py b/unified_planning/model/mixins/actions_set.py index a99f926bc..9e67709f2 100644 --- a/unified_planning/model/mixins/actions_set.py +++ b/unified_planning/model/mixins/actions_set.py @@ -83,23 +83,23 @@ def durative_actions(self) -> Iterator["up.model.action.DurativeAction"]: yield a @property - def processes(self) -> Iterator["up.model.transition.Process"]: + def processes(self) -> Iterator["up.model.natural_transition.Process"]: """Returs all the sensing actions of the problem. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" for a in self._actions: - if isinstance(a, up.model.transition.Process): + if isinstance(a, up.model.natural_transition.Process): yield a @property - def events(self) -> Iterator["up.model.transition.Event"]: + def events(self) -> Iterator["up.model.natural_transition.Event"]: """Returs all the sensing actions of the problem. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" for a in self._actions: - if isinstance(a, up.model.transition.Event): + if isinstance(a, up.model.natural_transition.Event): yield a @property diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py new file mode 100644 index 000000000..86d07e6a3 --- /dev/null +++ b/unified_planning/model/natural_transition.py @@ -0,0 +1,542 @@ +# 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 the `Action` base class and some of his extensions. +An `Action` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` +and a `list` of `effects`. +""" + + +import unified_planning as up +from unified_planning.environment import get_environment, Environment +from unified_planning.exceptions import ( + UPTypeError, + UPUnboundedVariablesError, + UPProblemDefinitionError, + UPUsageError, +) +from unified_planning.model.mixins.timed_conds_effs import TimedCondsEffs +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Set, Union, Optional, Iterable +from collections import OrderedDict + +from unified_planning.model.transition import Transition + + +""" +Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: +Processes or Events. +Processes dictate how numeric variables evolve over time through the use of time-derivative functions +Events dictate the analogous of urgent transitions in timed automata theory +""" + + +class NaturalTransition(Transition): + """This is the `NaturalTransition` interface""" + + +class Process(NaturalTransition): + """This is the `Process` class, which implements the abstract `NaturalTransition` class.""" + + def __init__( + self, + _name: str, + _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, + _env: Optional[Environment] = None, + **kwargs: "up.model.types.Type", + ): + Transition.__init__(self, _name, _parameters, _env, **kwargs) + self._preconditions: List["up.model.fnode.FNode"] = [] + self._effects: List[up.model.effect.Effect] = [] + self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None + # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment + self._fluents_assigned: Dict[ + "up.model.fnode.FNode", "up.model.fnode.FNode" + ] = {} + # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease + self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + + def __repr__(self) -> str: + s = [] + s.append(f"process {self.name}") + self._print_parameters(s) + s.append(" {\n") + s.append(" preconditions = [\n") + for c in self.preconditions: + s.append(f" {str(c)}\n") + s.append(" ]\n") + s.append(" effects = [\n") + for e in self.effects: + s.append(f" {str(e)}\n") + s.append(" ]\n") + s.append(" }") + return "".join(s) + + def __eq__(self, oth: object) -> bool: + if isinstance(oth, Process): + cond = ( + self._environment == oth._environment + and self._name == oth._name + and self._parameters == oth._parameters + ) + return ( + cond + and set(self._preconditions) == set(oth._preconditions) + and set(self._effects) == set(oth._effects) + and self._simulated_effect == oth._simulated_effect + ) + else: + return False + + def __hash__(self) -> int: + res = hash(self._name) + for ap in self._parameters.items(): + res += hash(ap) + for p in self._preconditions: + res += hash(p) + for e in self._effects: + res += hash(e) + res += hash(self._simulated_effect) + return res + + def clone(self): + new_params = OrderedDict( + (param_name, param.type) for param_name, param in self._parameters.items() + ) + new_process = Process(self._name, new_params, self._environment) + new_process._preconditions = self._preconditions[:] + new_process._effects = [e.clone() for e in self._effects] + new_process._fluents_assigned = self._fluents_assigned.copy() + new_process._fluents_inc_dec = self._fluents_inc_dec.copy() + new_process._simulated_effect = self._simulated_effect + return new_process + + @property + def preconditions(self) -> List["up.model.fnode.FNode"]: + """Returns the `list` of the `Process` `preconditions`.""" + return self._preconditions + + def clear_preconditions(self): + """Removes all the `Process preconditions`""" + self._preconditions = [] + + @property + def effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `Process effects`.""" + return self._effects + + def clear_effects(self): + """Removes all the `Process's effects`.""" + self._effects = [] + self._fluents_assigned = {} + self._fluents_inc_dec = set() + + def _add_effect_instance(self, effect: "up.model.effect.Effect"): + assert ( + effect.environment == self._environment + ), "effect does not have the same environment of the Process" + + self._effects.append(effect) + + def add_precondition( + self, + precondition: Union[ + "up.model.fnode.FNode", + "up.model.fluent.Fluent", + "up.model.parameter.Parameter", + bool, + ], + ): + """ + Adds the given expression to `Process's preconditions`. + + :param precondition: The expression that must be added to the `Process's preconditions`. + """ + (precondition_exp,) = self._environment.expression_manager.auto_promote( + precondition + ) + assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() + if precondition_exp == self._environment.expression_manager.TRUE(): + return + free_vars = self._environment.free_vars_oracle.get_free_variables( + precondition_exp + ) + if len(free_vars) != 0: + raise UPUnboundedVariablesError( + f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" + ) + if precondition_exp not in self._preconditions: + self._preconditions.append(precondition_exp) + + def add_derivative( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + ): + """ + Adds the given `time derivative effect` to the `process's effects`. + + :param fluent: The `fluent` is the numeric state variable of which this process expresses its time derivative, which in Newton's notation would be over-dot(fluent). + :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote( + fluent, + value, + True, + ) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." + ) + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"Process effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Derivative can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.DERIVATIVE, + forall=tuple(), + ) + ) + + +class Event(NaturalTransition): + """This class represents an event.""" + + def __init__( + self, + _name: str, + _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, + _env: Optional[Environment] = None, + **kwargs: "up.model.types.Type", + ): + Transition.__init__(self, _name, _parameters, _env, **kwargs) + self._preconditions: List["up.model.fnode.FNode"] = [] + self._effects: List[up.model.effect.Effect] = [] + self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None + # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment + self._fluents_assigned: Dict[ + "up.model.fnode.FNode", "up.model.fnode.FNode" + ] = {} + # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease + self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + + def __eq__(self, oth: object) -> bool: + if isinstance(oth, Event): + cond = ( + self._environment == oth._environment + and self._name == oth._name + and self._parameters == oth._parameters + ) + return ( + cond + and set(self._preconditions) == set(oth._preconditions) + and set(self._effects) == set(oth._effects) + and self._simulated_effect == oth._simulated_effect + ) + else: + return False + + def __hash__(self) -> int: + res = hash(self._name) + for ap in self._parameters.items(): + res += hash(ap) + for p in self._preconditions: + res += hash(p) + for e in self._effects: + res += hash(e) + res += hash(self._simulated_effect) + return res + + def clone(self): + new_params = OrderedDict( + (param_name, param.type) for param_name, param in self._parameters.items() + ) + new_event = Event(self._name, new_params, self._environment) + new_event._preconditions = self._preconditions[:] + new_event._effects = [e.clone() for e in self._effects] + new_event._fluents_assigned = self._fluents_assigned.copy() + new_event._fluents_inc_dec = self._fluents_inc_dec.copy() + new_event._simulated_effect = self._simulated_effect + return new_event + + @property + def preconditions(self) -> List["up.model.fnode.FNode"]: + """Returns the `list` of the `Action` `preconditions`.""" + return self._preconditions + + def clear_preconditions(self): + """Removes all the `Action preconditions`""" + self._preconditions = [] + + @property + def effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `Action effects`.""" + return self._effects + + def clear_effects(self): + """Removes all the `Action's effects`.""" + self._effects = [] + self._fluents_assigned = {} + self._fluents_inc_dec = set() + self._simulated_effect = None + + @property + def conditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action conditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if e.is_conditional()] + + def is_conditional(self) -> bool: + """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" + return any(e.is_conditional() for e in self._effects) + + @property + def unconditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action unconditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if not e.is_conditional()] + + def add_precondition( + self, + precondition: Union[ + "up.model.fnode.FNode", + "up.model.fluent.Fluent", + "up.model.parameter.Parameter", + bool, + ], + ): + """ + Adds the given expression to `action's preconditions`. + + :param precondition: The expression that must be added to the `action's preconditions`. + """ + (precondition_exp,) = self._environment.expression_manager.auto_promote( + precondition + ) + assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() + if precondition_exp == self._environment.expression_manager.TRUE(): + return + free_vars = self._environment.free_vars_oracle.get_free_variables( + precondition_exp + ) + if len(free_vars) != 0: + raise UPUnboundedVariablesError( + f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" + ) + if precondition_exp not in self._preconditions: + self._preconditions.append(precondition_exp) + + def add_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `assignment` to the `action's effects`. + + :param fluent: The `fluent` of which `value` is modified by the `assignment`. + :param value: The `value` to assign to the given `fluent`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." + ) + if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + # Value is not assignable to fluent (its type is not a subset of the fluent's type). + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + self._add_effect_instance( + up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) + ) + + def add_increase_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `increase effect` to the `action's effects`. + + :param fluent: The `fluent` which `value` is increased. + :param value: The given `fluent` is incremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote( + fluent, + value, + condition, + ) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Increase effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.INCREASE, + forall=forall, + ) + ) + + def add_decrease_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `decrease effect` to the `action's effects`. + + :param fluent: The `fluent` which value is decreased. + :param value: The given `fluent` is decremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Decrease effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.DECREASE, + forall=forall, + ) + ) + + def _add_effect_instance(self, effect: "up.model.effect.Effect"): + assert ( + effect.environment == self._environment + ), "effect does not have the same environment of the action" + up.model.effect.check_conflicting_effects( + effect, + None, + self._simulated_effect, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + self._effects.append(effect) + + @property + def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: + """Returns the `action` `simulated effect`.""" + return self._simulated_effect + + def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): + """ + Sets the given `simulated effect` as the only `action's simulated effect`. + + :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only + `simulated effect`. + """ + up.model.effect.check_conflicting_simulated_effects( + simulated_effect, + None, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + if simulated_effect.environment != self.environment: + raise UPUsageError( + "The added SimulatedEffect does not have the same environment of the Action" + ) + self._simulated_effect = simulated_effect + + def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): + self._preconditions = preconditions + + def __repr__(self) -> str: + s = [] + s.append(f"event {self.name}") + self._print_parameters(s) + s.append(" {\n") + s.append(" preconditions = [\n") + for c in self.preconditions: + s.append(f" {str(c)}\n") + s.append(" ]\n") + s.append(" effects = [\n") + for e in self.effects: + s.append(f" {str(e)}\n") + s.append(" ]\n") + if self._simulated_effect is not None: + s.append(f" simulated effect = {self._simulated_effect}\n") + s.append(" }") + return "".join(s) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 78a778c60..b74ef6045 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -322,7 +322,7 @@ def _get_static_and_unused_fluents( for f in a.simulated_effect.fluents: static_fluents.discard(f.fluent()) - elif isinstance(a, up.model.transition.Event): + elif isinstance(a, up.model.natural_transition.Event): # NOTE copypaste of above, with mixin should become one single block remove_used_fluents(*a.preconditions) for e in a.effects: @@ -344,7 +344,7 @@ def _get_static_and_unused_fluents( unused_fluents.clear() for f in se.fluents: static_fluents.discard(f.fluent()) - elif isinstance(a, up.model.transition.Process): + elif isinstance(a, up.model.natural_transition.Process): for e in a.effects: remove_used_fluents(e.fluent, e.value, e.condition) static_fluents.discard(e.fluent.fluent()) @@ -1022,9 +1022,9 @@ def update_problem_kind_action( if len(action.simulated_effects) > 0: self.kind.set_simulated_entities("SIMULATED_EFFECTS") self.kind.set_time("CONTINUOUS_TIME") - elif isinstance(action, up.model.transition.Process): + elif isinstance(action, up.model.natural_transition.Process): pass # TODO add Process kind - elif isinstance(action, up.model.transition.Event): + elif isinstance(action, up.model.natural_transition.Event): pass # TODO add Event kind else: raise NotImplementedError diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py index cf9444f92..5c3fe6c16 100644 --- a/unified_planning/model/transition.py +++ b/unified_planning/model/transition.py @@ -152,506 +152,3 @@ def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": def is_conditional(self) -> bool: """Returns `True` if the `Action` has `conditional effects`, `False` otherwise.""" raise NotImplementedError - - -""" -Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: -Processes or Events. -Processes dictate how numeric variables evolve over time through the use of time-derivative functions -Events dictate the analogous of urgent transitions in timed automata theory -""" - - -class Process(Transition): - """This is the `Process` class, which implements the abstract `Action` class.""" - - def __init__( - self, - _name: str, - _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, - _env: Optional[Environment] = None, - **kwargs: "up.model.types.Type", - ): - Transition.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] - self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None - # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment - self._fluents_assigned: Dict[ - "up.model.fnode.FNode", "up.model.fnode.FNode" - ] = {} - # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease - self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() - - def __repr__(self) -> str: - s = [] - s.append(f"process {self.name}") - self._print_parameters(s) - s.append(" {\n") - s.append(" preconditions = [\n") - for c in self.preconditions: - s.append(f" {str(c)}\n") - s.append(" ]\n") - s.append(" effects = [\n") - for e in self.effects: - s.append(f" {str(e)}\n") - s.append(" ]\n") - s.append(" }") - return "".join(s) - - def __eq__(self, oth: object) -> bool: - if isinstance(oth, Process): - cond = ( - self._environment == oth._environment - and self._name == oth._name - and self._parameters == oth._parameters - ) - return ( - cond - and set(self._preconditions) == set(oth._preconditions) - and set(self._effects) == set(oth._effects) - and self._simulated_effect == oth._simulated_effect - ) - else: - return False - - def __hash__(self) -> int: - res = hash(self._name) - for ap in self._parameters.items(): - res += hash(ap) - for p in self._preconditions: - res += hash(p) - for e in self._effects: - res += hash(e) - res += hash(self._simulated_effect) - return res - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_process = Process(self._name, new_params, self._environment) - new_process._preconditions = self._preconditions[:] - new_process._effects = [e.clone() for e in self._effects] - new_process._fluents_assigned = self._fluents_assigned.copy() - new_process._fluents_inc_dec = self._fluents_inc_dec.copy() - new_process._simulated_effect = self._simulated_effect - return new_process - - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Process` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Process preconditions`""" - self._preconditions = [] - - @property - def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Process effects`.""" - return self._effects - - def clear_effects(self): - """Removes all the `Process's effects`.""" - self._effects = [] - self._fluents_assigned = {} - self._fluents_inc_dec = set() - - def _add_effect_instance(self, effect: "up.model.effect.Effect"): - assert ( - effect.environment == self._environment - ), "effect does not have the same environment of the Process" - - self._effects.append(effect) - - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `Process's preconditions`. - - :param precondition: The expression that must be added to the `Process's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - - def add_derivative( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - ): - """ - Adds the given `time derivative effect` to the `process's effects`. - - :param fluent: The `fluent` is the numeric state variable of which this process expresses its time derivative, which in Newton's notation would be over-dot(fluent). - :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote( - fluent, - value, - True, - ) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." - ) - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"Process effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Derivative can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DERIVATIVE, - forall=tuple(), - ) - ) - - -class Event(Transition): - """This class represents an event.""" - - def __init__( - self, - _name: str, - _parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None, - _env: Optional[Environment] = None, - **kwargs: "up.model.types.Type", - ): - Transition.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] - self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None - # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment - self._fluents_assigned: Dict[ - "up.model.fnode.FNode", "up.model.fnode.FNode" - ] = {} - # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease - self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() - - def __eq__(self, oth: object) -> bool: - if isinstance(oth, Event): - cond = ( - self._environment == oth._environment - and self._name == oth._name - and self._parameters == oth._parameters - ) - return ( - cond - and set(self._preconditions) == set(oth._preconditions) - and set(self._effects) == set(oth._effects) - and self._simulated_effect == oth._simulated_effect - ) - else: - return False - - def __hash__(self) -> int: - res = hash(self._name) - for ap in self._parameters.items(): - res += hash(ap) - for p in self._preconditions: - res += hash(p) - for e in self._effects: - res += hash(e) - res += hash(self._simulated_effect) - return res - - def clone(self): - new_params = OrderedDict( - (param_name, param.type) for param_name, param in self._parameters.items() - ) - new_event = Event(self._name, new_params, self._environment) - new_event._preconditions = self._preconditions[:] - new_event._effects = [e.clone() for e in self._effects] - new_event._fluents_assigned = self._fluents_assigned.copy() - new_event._fluents_inc_dec = self._fluents_inc_dec.copy() - new_event._simulated_effect = self._simulated_effect - return new_event - - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Action` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Action preconditions`""" - self._preconditions = [] - - @property - def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Action effects`.""" - return self._effects - - def clear_effects(self): - """Removes all the `Action's effects`.""" - self._effects = [] - self._fluents_assigned = {} - self._fluents_inc_dec = set() - self._simulated_effect = None - - @property - def conditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action conditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if e.is_conditional()] - - def is_conditional(self) -> bool: - """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" - return any(e.is_conditional() for e in self._effects) - - @property - def unconditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action unconditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if not e.is_conditional()] - - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `action's preconditions`. - - :param precondition: The expression that must be added to the `action's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - - def add_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `assignment` to the `action's effects`. - - :param fluent: The `fluent` of which `value` is modified by the `assignment`. - :param value: The `value` to assign to the given `fluent`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." - ) - if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - # Value is not assignable to fluent (its type is not a subset of the fluent's type). - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - self._add_effect_instance( - up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) - ) - - def add_increase_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `increase effect` to the `action's effects`. - - :param fluent: The `fluent` which `value` is increased. - :param value: The given `fluent` is incremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote( - fluent, - value, - condition, - ) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Increase effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.INCREASE, - forall=forall, - ) - ) - - def add_decrease_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `decrease effect` to the `action's effects`. - - :param fluent: The `fluent` which value is decreased. - :param value: The given `fluent` is decremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Decrease effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DECREASE, - forall=forall, - ) - ) - - def _add_effect_instance(self, effect: "up.model.effect.Effect"): - assert ( - effect.environment == self._environment - ), "effect does not have the same environment of the action" - up.model.effect.check_conflicting_effects( - effect, - None, - self._simulated_effect, - self._fluents_assigned, - self._fluents_inc_dec, - "action", - ) - self._effects.append(effect) - - @property - def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: - """Returns the `action` `simulated effect`.""" - return self._simulated_effect - - def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): - """ - Sets the given `simulated effect` as the only `action's simulated effect`. - - :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only - `simulated effect`. - """ - up.model.effect.check_conflicting_simulated_effects( - simulated_effect, - None, - self._fluents_assigned, - self._fluents_inc_dec, - "action", - ) - if simulated_effect.environment != self.environment: - raise UPUsageError( - "The added SimulatedEffect does not have the same environment of the Action" - ) - self._simulated_effect = simulated_effect - - def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): - self._preconditions = preconditions - - def __repr__(self) -> str: - s = [] - s.append(f"event {self.name}") - self._print_parameters(s) - s.append(" {\n") - s.append(" preconditions = [\n") - for c in self.preconditions: - s.append(f" {str(c)}\n") - s.append(" ]\n") - s.append(" effects = [\n") - for e in self.effects: - s.append(f" {str(e)}\n") - s.append(" ]\n") - if self._simulated_effect is not None: - s.append(f" simulated effect = {self._simulated_effect}\n") - s.append(" }") - return "".join(s) diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index eaee2ffae..9a4893b8d 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -1,5 +1,5 @@ from unified_planning.shortcuts import * -from unified_planning.model.transition import Process +from unified_planning.model.natural_transition import Process from unified_planning.test import TestCase From dd896db0dd0011331f2adba9bc3fc1ba807ffa4d Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Mon, 2 Dec 2024 11:59:37 +0100 Subject: [PATCH 07/33] refactoring: mypy fixes and new set mixin for natural transitions --- unified_planning/io/pddl_reader.py | 4 +- unified_planning/io/pddl_writer.py | 114 ++++++++++----- unified_planning/model/mixins/__init__.py | 4 + unified_planning/model/mixins/actions_set.py | 23 +-- .../model/mixins/natural_transitions_set.py | 136 ++++++++++++++++++ unified_planning/model/problem.py | 6 + unified_planning/test/examples/processes.py | 4 +- unified_planning/test/test_pddl_io.py | 28 +++- 8 files changed, 254 insertions(+), 65 deletions(-) create mode 100644 unified_planning/model/mixins/natural_transitions_set.py diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index 761883374..d48ebf627 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -1292,7 +1292,7 @@ def declare_type( CustomParseResults(a["eff"][0]), domain_str, ) - problem.add_action(proc) + problem.add_natural_transition(proc) for a in domain_res.get("events", []): n = a["name"] @@ -1317,7 +1317,7 @@ def declare_type( CustomParseResults(a["eff"][0]), domain_str, ) - problem.add_action(evt) + problem.add_natural_transition(evt) for a in domain_res.get("actions", []): n = a["name"] diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 8110da3e2..4f1f3d435 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -23,6 +23,10 @@ import unified_planning as up import unified_planning.environment +from unified_planning.model.action import Action +from unified_planning.model.fnode import FNode +from unified_planning.model.natural_transition import NaturalTransition +from unified_planning.model.transition import Transition import unified_planning.model.walkers as walkers from unified_planning.model import ( InstantaneousAction, @@ -139,6 +143,7 @@ WithName = Union[ "up.model.Type", "up.model.Action", + "up.model.NaturalTransition", "up.model.Fluent", "up.model.Object", "up.model.Parameter", @@ -530,7 +535,9 @@ def _write_domain(self, out: IO[str]): converter = ConverterToPDDLString( self.problem.environment, self._get_mangled_name ) - costs = {} + costs: Dict[ + Union[NaturalTransition, Action], Optional[FNode] + ] = {} # TODO check if natural_transition should be here metrics = self.problem.quality_metrics if len(metrics) == 1: metric = metrics[0] @@ -595,15 +602,10 @@ def _write_domain(self, out: IO[str]): out.write(")\n") for a in self.problem.actions: - if isinstance(a, up.model.InstantaneousAction) or isinstance( - a, up.model.Event - ): + if isinstance(a, up.model.InstantaneousAction): if any(p.simplify().is_false() for p in a.preconditions): continue - if isinstance(a, up.model.Event): - out.write(f" (:event {self._get_mangled_name(a)}") - else: - out.write(f" (:action {self._get_mangled_name(a)}") + out.write(f" (:action {self._get_mangled_name(a)}") out.write(f"\n :parameters (") self._write_parameters(out, a) out.write(")") @@ -636,34 +638,6 @@ def _write_domain(self, out: IO[str]): ) out.write(")") out.write(")\n") - elif isinstance(a, up.model.Process): - if any(p.simplify().is_false() for p in a.preconditions): - continue - out.write(f" (:process {self._get_mangled_name(a)}") - out.write(f"\n :parameters (") - self._write_parameters(out, a) - out.write(")") - if len(a.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in a.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(a.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") - if len(a.effects) > 0: - out.write("\n :effect (and") - for e in a.effects: - _write_derivative( - e, - out, - converter, - ) - out.write(")") - out.write(")\n") elif isinstance(a, DurativeAction): if any( c.simplify().is_false() for cl in a.conditions.values() for c in cl @@ -733,6 +707,74 @@ def _write_domain(self, out: IO[str]): out.write(")\n") else: raise NotImplementedError + + for nt in self.problem.natural_transitions: + if isinstance(nt, up.model.Event): + if any(p.simplify().is_false() for p in nt.preconditions): + continue + out.write(f" (:event {self._get_mangled_name(nt)}") + out.write(f"\n :parameters (") + self._write_parameters(out, nt) + out.write(")") + if len(nt.preconditions) > 0: + precond_str = [] + for p in (c.simplify() for c in nt.preconditions): + if not p.is_true(): + if p.is_and(): + precond_str.extend(map(converter.convert, p.args)) + else: + precond_str.append(converter.convert(p)) + out.write(f'\n :precondition (and {" ".join(precond_str)})') + elif len(nt.preconditions) == 0 and self.empty_preconditions: + out.write(f"\n :precondition ()") + if len(nt.effects) > 0: + out.write("\n :effect (and") + for e in nt.effects: + _write_effect( + e, + None, + out, + converter, + self.rewrite_bool_assignments, + self._get_mangled_name, + ) + + if nt in costs: + out.write( + f" (increase (total-cost) {converter.convert(costs[nt])})" + ) + out.write(")") + out.write(")\n") + elif isinstance(nt, up.model.Process): + if any(p.simplify().is_false() for p in nt.preconditions): + continue + out.write(f" (:process {self._get_mangled_name(nt)}") + out.write(f"\n :parameters (") + self._write_parameters(out, nt) + out.write(")") + if len(nt.preconditions) > 0: + precond_str = [] + for p in (c.simplify() for c in nt.preconditions): + if not p.is_true(): + if p.is_and(): + precond_str.extend(map(converter.convert, p.args)) + else: + precond_str.append(converter.convert(p)) + out.write(f'\n :precondition (and {" ".join(precond_str)})') + elif len(nt.preconditions) == 0 and self.empty_preconditions: + out.write(f"\n :precondition ()") + if len(nt.effects) > 0: + out.write("\n :effect (and") + for e in nt.effects: + _write_derivative( + e, + out, + converter, + ) + out.write(")") + out.write(")\n") + else: + raise NotImplementedError out.write(")\n") def _write_problem(self, out: IO[str]): diff --git a/unified_planning/model/mixins/__init__.py b/unified_planning/model/mixins/__init__.py index 3cd6ed8e7..38f96009b 100644 --- a/unified_planning/model/mixins/__init__.py +++ b/unified_planning/model/mixins/__init__.py @@ -14,6 +14,9 @@ # from unified_planning.model.mixins.actions_set import ActionsSetMixin +from unified_planning.model.mixins.natural_transitions_set import ( + NaturalTransitionsSetMixin, +) from unified_planning.model.mixins.time_model import TimeModelMixin from unified_planning.model.mixins.fluents_set import FluentsSetMixin from unified_planning.model.mixins.objects_set import ObjectsSetMixin @@ -24,6 +27,7 @@ __all__ = [ "ActionsSetMixin", + "NaturalTransitionsSetMixin", "TimeModelMixin", "FluentsSetMixin", "ObjectsSetMixin", diff --git a/unified_planning/model/mixins/actions_set.py b/unified_planning/model/mixins/actions_set.py index 9e67709f2..2c5be9272 100644 --- a/unified_planning/model/mixins/actions_set.py +++ b/unified_planning/model/mixins/actions_set.py @@ -82,26 +82,6 @@ def durative_actions(self) -> Iterator["up.model.action.DurativeAction"]: if isinstance(a, up.model.action.DurativeAction): yield a - @property - def processes(self) -> Iterator["up.model.natural_transition.Process"]: - """Returs all the sensing actions of the problem. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - for a in self._actions: - if isinstance(a, up.model.natural_transition.Process): - yield a - - @property - def events(self) -> Iterator["up.model.natural_transition.Event"]: - """Returs all the sensing actions of the problem. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - for a in self._actions: - if isinstance(a, up.model.natural_transition.Event): - yield a - @property def conditional_actions(self) -> List["up.model.action.Action"]: """ @@ -147,8 +127,7 @@ def has_action(self, name: str) -> bool: return True return False - # TODO different add_action and add_*something_else* - def add_action(self, action: "up.model.transition.Transition"): + def add_action(self, action: "up.model.action.Action"): """ Adds the given `action` to the `problem`. diff --git a/unified_planning/model/mixins/natural_transitions_set.py b/unified_planning/model/mixins/natural_transitions_set.py new file mode 100644 index 000000000..2f2e8bf1c --- /dev/null +++ b/unified_planning/model/mixins/natural_transitions_set.py @@ -0,0 +1,136 @@ +# 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 warnings import warn +import unified_planning as up +from unified_planning.exceptions import UPProblemDefinitionError, UPValueError +from typing import Iterator, List, Iterable + + +class NaturalTransitionsSetMixin: + """ + This class is a mixin that contains a `set` of `natural_transitions` with some related methods. + + NOTE: when this mixin is used in combination with other mixins that share some + of the attributes (e.g. `environment`, `add_user_type_method`, `has_name_method`), it is required + to pass the very same arguments to the mixins constructors. + """ + + def __init__(self, environment, add_user_type_method, has_name_method): + self._env = environment + self._add_user_type_method = add_user_type_method + self._has_name_method = has_name_method + self._natural_transitions: List[ + "up.model.natural_transition.NaturalTransition" + ] = [] + + @property + def environment(self) -> "up.environment.Environment": + """Returns the `Problem` environment.""" + return self._env + + @property + def natural_transitions( + self, + ) -> List["up.model.natural_transition.NaturalTransition"]: + """Returns the list of the `NaturalTransitions` in the `Problem`.""" + return self._natural_transitions + + def clear_natural_transitions(self): + """Removes all the `Problem` `NaturalTransitions`.""" + self._natural_transitions = [] + + @property + def processes(self) -> Iterator["up.model.natural_transition.Process"]: + """Returs all the processes of the problem. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + for a in self._natural_transitions: + if isinstance(a, up.model.natural_transition.Process): + yield a + + @property + def events(self) -> Iterator["up.model.natural_transition.Event"]: + """Returs all the events of the problem. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + for a in self._natural_transitions: + if isinstance(a, up.model.natural_transition.Event): + yield a + + def natural_transition( + self, name: str + ) -> "up.model.natural_transition.NaturalTransition": + """ + Returns the `natural_transition` with the given `name`. + + :param name: The `name` of the target `natural_transition`. + :return: The `natural_transition` in the `problem` with the given `name`. + """ + for a in self._natural_transitions: + if a.name == name: + return a + raise UPValueError(f"NaturalTransition of name: {name} is not defined!") + + def has_natural_transition(self, name: str) -> bool: + """ + Returns `True` if the `problem` has the `natural_transition` with the given `name`, + `False` otherwise. + + :param name: The `name` of the target `natural_transition`. + :return: `True` if the `problem` has an `natural_transition` with the given `name`, `False` otherwise. + """ + for a in self._natural_transitions: + if a.name == name: + return True + return False + + def add_natural_transition( + self, natural_transition: "up.model.natural_transition.NaturalTransition" + ): + """ + Adds the given `natural_transition` to the `problem`. + + :param natural_transition: The `natural_transition` that must be added to the `problem`. + """ + assert ( + natural_transition.environment == self._env + ), "NaturalTransition does not have the same environment of the problem" + if self._has_name_method(natural_transition.name): + msg = f"Name {natural_transition.name} already defined! Different elements of a problem can have the same name if the environment flag error_used_name is disabled." + if self._env.error_used_name or any( + natural_transition.name == a.name for a in self._natural_transitions + ): + raise UPProblemDefinitionError(msg) + else: + warn(msg) + self._natural_transitions.append(natural_transition) + for param in natural_transition.parameters: + if param.type.is_user_type(): + self._add_user_type_method(param.type) + + def add_natural_transitions( + self, + natural_transitions: Iterable["up.model.natural_transition.NaturalTransition"], + ): + """ + Adds the given `natural_transitions` to the `problem`. + + :param natural_transitions: The `natural_transitions` that must be added to the `problem`. + """ + for natural_transition in natural_transitions: + self.add_natural_transition(natural_transition) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index b74ef6045..07eb42743 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -22,6 +22,7 @@ from unified_planning.model.abstract_problem import AbstractProblem from unified_planning.model.mixins import ( ActionsSetMixin, + NaturalTransitionsSetMixin, TimeModelMixin, FluentsSetMixin, ObjectsSetMixin, @@ -52,6 +53,7 @@ class Problem( # type: ignore[misc] TimeModelMixin, FluentsSetMixin, ActionsSetMixin, + NaturalTransitionsSetMixin, ObjectsSetMixin, InitialStateMixin, MetricsMixin, @@ -80,6 +82,9 @@ def __init__( ActionsSetMixin.__init__( self, self.environment, self._add_user_type, self.has_name ) + NaturalTransitionsSetMixin.__init__( + self, self.environment, self._add_user_type, self.has_name + ) ObjectsSetMixin.__init__( self, self.environment, self._add_user_type, self.has_name ) @@ -235,6 +240,7 @@ def clone(self): TimeModelMixin._clone_to(self, new_p) new_p._actions = [a.clone() for a in self._actions] + new_p._natural_transitions = [a.clone() for a in self._natural_transitions] new_p._timed_effects = { t: [e.clone() for e in el] for t, el in self._timed_effects.items() } diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 9a4893b8d..2da9f1fdb 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -24,8 +24,8 @@ def get_example_problems(): problem.add_fluent(on) problem.add_fluent(d) problem.add_action(a) - problem.add_action(b) - problem.add_action(evt) + problem.add_natural_transition(b) + problem.add_natural_transition(evt) problem.set_initial_value(on, False) problem.set_initial_value(d, 0) problem.add_goal(GE(d, 10)) diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 198663512..bb5b59faf 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -443,13 +443,31 @@ def test_non_linear_car(self): self.assertTrue(problem is not None) self.assertEqual(len(problem.fluents), 8) self.assertEqual( - len(list([ele for ele in problem.actions if isinstance(ele, Process)])), 3 + len( + list( + [ + ele + for ele in problem.natural_transitions + if isinstance(ele, Process) + ] + ) + ), + 3, ) self.assertEqual( - len(list([ele for ele in problem.actions if isinstance(ele, Event)])), 1 + len( + list( + [ + ele + for ele in problem.natural_transitions + if isinstance(ele, Event) + ] + ) + ), + 1, ) found_drag_ahead = False - for ele in problem.actions: + for ele in problem.natural_transitions: if isinstance(ele, Process): for e in ele.effects: self.assertEqual(e.kind, EffectKind.DERIVATIVE) @@ -595,6 +613,10 @@ def test_examples_io(self): ) ) self.assertEqual(len(problem.actions), len(parsed_problem.actions)) + self.assertEqual( + len(problem.natural_transitions), + len(parsed_problem.natural_transitions), + ) for a in problem.actions: parsed_a = parsed_problem.action(w.get_pddl_name(a)) self.assertEqual(a, w.get_item_named(parsed_a.name)) From 3a550073ea082af6b1f54ee93a010161ccaca878 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 09:39:13 +0100 Subject: [PATCH 08/33] refactoring: minor changes and edited comments --- unified_planning/io/pddl_writer.py | 6 +-- unified_planning/model/natural_transition.py | 57 ++++++++++---------- unified_planning/model/transition.py | 36 ++++++------- unified_planning/test/examples/processes.py | 3 +- unified_planning/test/test_pddl_io.py | 28 +++------- 5 files changed, 57 insertions(+), 73 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 4f1f3d435..2ee619cef 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -23,10 +23,6 @@ import unified_planning as up import unified_planning.environment -from unified_planning.model.action import Action -from unified_planning.model.fnode import FNode -from unified_planning.model.natural_transition import NaturalTransition -from unified_planning.model.transition import Transition import unified_planning.model.walkers as walkers from unified_planning.model import ( InstantaneousAction, @@ -536,7 +532,7 @@ def _write_domain(self, out: IO[str]): self.problem.environment, self._get_mangled_name ) costs: Dict[ - Union[NaturalTransition, Action], Optional[FNode] + Union[up.model.NaturalTransition, up.model.Action], Optional[up.model.FNode] ] = {} # TODO check if natural_transition should be here metrics = self.problem.quality_metrics if len(metrics) == 1: diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 86d07e6a3..06d38c0ff 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -12,11 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -""" -This module defines the `Action` base class and some of his extensions. -An `Action` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` -and a `list` of `effects`. -""" import unified_planning as up @@ -34,6 +29,14 @@ from unified_planning.model.transition import Transition +# TODO fix comments here +# TODO check weather events/processes should have all the methods/other stuff that actions have. If yes, we need more tests. + +""" +This module defines the `NaturalTransition` class and some of his extensions. +An `NaturalTransition` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` +and a `list` of `effects`. +""" """ Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: @@ -283,20 +286,20 @@ def clone(self): @property def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Action` `preconditions`.""" + """Returns the `list` of the `Event` `preconditions`.""" return self._preconditions def clear_preconditions(self): - """Removes all the `Action preconditions`""" + """Removes all the `Event preconditions`""" self._preconditions = [] @property def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Action effects`.""" + """Returns the `list` of the `Event effects`.""" return self._effects def clear_effects(self): - """Removes all the `Action's effects`.""" + """Removes all the `Event's effects`.""" self._effects = [] self._fluents_assigned = {} self._fluents_inc_dec = set() @@ -304,19 +307,19 @@ def clear_effects(self): @property def conditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action conditional effects`. + """Returns the `list` of the `event conditional effects`. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" return [e for e in self._effects if e.is_conditional()] def is_conditional(self) -> bool: - """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" + """Returns `True` if the `event` has `conditional effects`, `False` otherwise.""" return any(e.is_conditional() for e in self._effects) @property def unconditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action unconditional effects`. + """Returns the `list` of the `event unconditional effects`. IMPORTANT NOTE: this property does some computation, so it should be called as seldom as possible.""" @@ -332,9 +335,9 @@ def add_precondition( ], ): """ - Adds the given expression to `action's preconditions`. + Adds the given expression to `event's preconditions`. - :param precondition: The expression that must be added to the `action's preconditions`. + :param precondition: The expression that must be added to the `event's preconditions`. """ (precondition_exp,) = self._environment.expression_manager.auto_promote( precondition @@ -360,7 +363,7 @@ def add_effect( forall: Iterable["up.model.variable.Variable"] = tuple(), ): """ - Adds the given `assignment` to the `action's effects`. + Adds the given `assignment` to the `event's effects`. :param fluent: The `fluent` of which `value` is modified by the `assignment`. :param value: The `value` to assign to the given `fluent`. @@ -383,7 +386,7 @@ def add_effect( if not fluent_exp.type.is_compatible(value_exp.type): # Value is not assignable to fluent (its type is not a subset of the fluent's type). raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" ) self._add_effect_instance( up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) @@ -397,7 +400,7 @@ def add_increase_effect( forall: Iterable["up.model.variable.Variable"] = tuple(), ): """ - Adds the given `increase effect` to the `action's effects`. + Adds the given `increase effect` to the `event's effects`. :param fluent: The `fluent` which `value` is increased. :param value: The given `fluent` is incremented by the given `value`. @@ -423,7 +426,7 @@ def add_increase_effect( raise UPTypeError("Effect condition is not a Boolean condition!") if not fluent_exp.type.is_compatible(value_exp.type): raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" ) if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): raise UPTypeError("Increase effects can be created only on numeric types!") @@ -445,7 +448,7 @@ def add_decrease_effect( forall: Iterable["up.model.variable.Variable"] = tuple(), ): """ - Adds the given `decrease effect` to the `action's effects`. + Adds the given `decrease effect` to the `event's effects`. :param fluent: The `fluent` which value is decreased. :param value: The given `fluent` is decremented by the given `value`. @@ -467,7 +470,7 @@ def add_decrease_effect( raise UPTypeError("Effect condition is not a Boolean condition!") if not fluent_exp.type.is_compatible(value_exp.type): raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" ) if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): raise UPTypeError("Decrease effects can be created only on numeric types!") @@ -484,27 +487,27 @@ def add_decrease_effect( def _add_effect_instance(self, effect: "up.model.effect.Effect"): assert ( effect.environment == self._environment - ), "effect does not have the same environment of the action" + ), "effect does not have the same environment of the event" up.model.effect.check_conflicting_effects( effect, None, self._simulated_effect, self._fluents_assigned, self._fluents_inc_dec, - "action", + "event", ) self._effects.append(effect) @property def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: - """Returns the `action` `simulated effect`.""" + """Returns the `event` `simulated effect`.""" return self._simulated_effect def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): """ - Sets the given `simulated effect` as the only `action's simulated effect`. + Sets the given `simulated effect` as the only `event's simulated effect`. - :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only + :param simulated_effect: The `SimulatedEffect` instance that must be set as this `event`'s only `simulated effect`. """ up.model.effect.check_conflicting_simulated_effects( @@ -512,11 +515,11 @@ def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffec None, self._fluents_assigned, self._fluents_inc_dec, - "action", + "event", ) if simulated_effect.environment != self.environment: raise UPUsageError( - "The added SimulatedEffect does not have the same environment of the Action" + "The added SimulatedEffect does not have the same environment of the Event" ) self._simulated_effect = simulated_effect diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py index 5c3fe6c16..f9d745ace 100644 --- a/unified_planning/model/transition.py +++ b/unified_planning/model/transition.py @@ -13,8 +13,8 @@ # limitations under the License. # """ -This module defines the `Action` base class and some of his extensions. -An `Action` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` +This module defines the `Transition` base class and some of his extensions. +A `Transition` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` and a `list` of `effects`. """ @@ -34,7 +34,7 @@ class Transition(ABC): - """This is the `Action` interface.""" + """This is the `Transition` interface.""" def __init__( self, @@ -53,7 +53,7 @@ def __init__( for n, t in _parameters.items(): assert self._environment.type_manager.has_type( t - ), "type of parameter does not belong to the same environment of the action" + ), "type of parameter does not belong to the same environment of the transition" self._parameters[n] = up.model.parameter.Parameter( n, t, self._environment ) @@ -61,7 +61,7 @@ def __init__( for n, t in kwargs.items(): assert self._environment.type_manager.has_type( t - ), "type of parameter does not belong to the same environment of the action" + ), "type of parameter does not belong to the same environment of the transition" self._parameters[n] = up.model.parameter.Parameter( n, t, self._environment ) @@ -92,49 +92,49 @@ def clone(self): @property def environment(self) -> Environment: - """Returns this `Action` `Environment`.""" + """Returns this `Transition` `Environment`.""" return self._environment @property def name(self) -> str: - """Returns the `Action` `name`.""" + """Returns the `Transition `name`.""" return self._name @name.setter def name(self, new_name: str): - """Sets the `Action` `name`.""" + """Sets the `Transition` `name`.""" self._name = new_name @property def parameters(self) -> List["up.model.parameter.Parameter"]: - """Returns the `list` of the `Action parameters`.""" + """Returns the `list` of the `Transition parameters`.""" return list(self._parameters.values()) def parameter(self, name: str) -> "up.model.parameter.Parameter": """ - Returns the `parameter` of the `Action` with the given `name`. + Returns the `parameter` of the `Transition` with the given `name`. Example ------- >>> from unified_planning.shortcuts import * >>> location_type = UserType("Location") >>> move = InstantaneousAction("move", source=location_type, target=location_type) - >>> move.parameter("source") # return the "source" parameter of the action, with type "Location" + >>> move.parameter("source") # return the "source" parameter of the transition, with type "Location" Location source >>> move.parameter("target") Location target - If a parameter's name (1) does not conflict with an existing attribute of `Action` and (2) does not start with '_' - it can also be accessed as if it was an attribute of the action. For instance: + If a parameter's name (1) does not conflict with an existing attribute of `Transition` and (2) does not start with '_' + it can also be accessed as if it was an attribute of the transition. For instance: >>> move.source Location source :param name: The `name` of the target `parameter`. - :return: The `parameter` of the `Action` with the given `name`. + :return: The `parameter` of the `Transition` with the given `name`. """ if name not in self._parameters: - raise ValueError(f"Action '{self.name}' has no parameter '{name}'") + raise ValueError(f"Transition '{self.name}' has no parameter '{name}'") return self._parameters[name] def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": @@ -142,13 +142,13 @@ def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": # guard access as pickling relies on attribute error to be thrown even when # no attributes of the object have been set. # In this case accessing `self._name` or `self._parameters`, would re-invoke __getattr__ - raise AttributeError(f"Action has no attribute '{parameter_name}'") + raise AttributeError(f"Transition has no attribute '{parameter_name}'") if parameter_name not in self._parameters: raise AttributeError( - f"Action '{self.name}' has no attribute or parameter '{parameter_name}'" + f"Transition '{self.name}' has no attribute or parameter '{parameter_name}'" ) return self._parameters[parameter_name] def is_conditional(self) -> bool: - """Returns `True` if the `Action` has `conditional effects`, `False` otherwise.""" + """Returns `True` if the `Transition` has `conditional effects`, `False` otherwise.""" raise NotImplementedError diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 2da9f1fdb..04e680621 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -1,7 +1,8 @@ from unified_planning.shortcuts import * -from unified_planning.model.natural_transition import Process from unified_planning.test import TestCase +# TODO we need more tests for better coverage + def get_example_problems(): problems = {} diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index bb5b59faf..20ef0dd2e 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -442,30 +442,14 @@ def test_non_linear_car(self): self.assertTrue(problem is not None) self.assertEqual(len(problem.fluents), 8) - self.assertEqual( - len( - list( - [ - ele - for ele in problem.natural_transitions - if isinstance(ele, Process) - ] - ) - ), - 3, + n_proc = len( + list([el for el in problem.natural_transitions if isinstance(el, Process)]) ) - self.assertEqual( - len( - list( - [ - ele - for ele in problem.natural_transitions - if isinstance(ele, Event) - ] - ) - ), - 1, + n_eve = len( + list([el for el in problem.natural_transitions if isinstance(el, Event)]) ) + self.assertEqual(n_proc, 3) + self.assertEqual(n_eve, 1) found_drag_ahead = False for ele in problem.natural_transitions: if isinstance(ele, Process): From 3e8b551d745834467ebbfdc8240a8710a2b2a0cc Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 09:59:44 +0100 Subject: [PATCH 09/33] refactoring: comments --- unified_planning/io/pddl_writer.py | 2 +- unified_planning/model/action.py | 4 +--- unified_planning/model/natural_transition.py | 12 +++--------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 2ee619cef..09fdd9bbb 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -533,7 +533,7 @@ def _write_domain(self, out: IO[str]): ) costs: Dict[ Union[up.model.NaturalTransition, up.model.Action], Optional[up.model.FNode] - ] = {} # TODO check if natural_transition should be here + ] = {} metrics = self.problem.quality_metrics if len(metrics) == 1: metric = metrics[0] diff --git a/unified_planning/model/action.py b/unified_planning/model/action.py index a9c23c1ce..112e15800 100644 --- a/unified_planning/model/action.py +++ b/unified_planning/model/action.py @@ -13,9 +13,7 @@ # limitations under the License. # """ -This module defines the `Action` base class and some of his extensions. -An `Action` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` -and a `list` of `effects`. +This module defines the `Action` class and some of his extensions. """ diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 06d38c0ff..6c0a982df 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -29,17 +29,11 @@ from unified_planning.model.transition import Transition -# TODO fix comments here -# TODO check weather events/processes should have all the methods/other stuff that actions have. If yes, we need more tests. +# TODO check weather events/processes should have all the methods/other stuff that actions have. +# If yes, we probably need more tests for better coverage """ -This module defines the `NaturalTransition` class and some of his extensions. -An `NaturalTransition` has a `name`, a `list` of `Parameter`, a `list` of `preconditions` -and a `list` of `effects`. -""" - -""" -Below we have natural transitions. These are not controlled by the agent and would probably need a proper subclass. Natural transitions can be of two kinds: +Below we have natural transitions. These are not controlled by the agent. Natural transitions can be of two kinds: Processes or Events. Processes dictate how numeric variables evolve over time through the use of time-derivative functions Events dictate the analogous of urgent transitions in timed automata theory From f7f68202df5d19487a984cab64d5c05a4a10d792 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 10:08:08 +0100 Subject: [PATCH 10/33] refactoring: correctly rewriting of _get_static_and_unused_fluents method in problem --- unified_planning/model/problem.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 07eb42743..f5e7adcd9 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -327,18 +327,6 @@ def _get_static_and_unused_fluents( unused_fluents.clear() for f in a.simulated_effect.fluents: static_fluents.discard(f.fluent()) - - elif isinstance(a, up.model.natural_transition.Event): - # NOTE copypaste of above, with mixin should become one single block - remove_used_fluents(*a.preconditions) - for e in a.effects: - remove_used_fluents(e.fluent, e.value, e.condition) - static_fluents.discard(e.fluent.fluent()) - if a.simulated_effect is not None: - # empty the set because a simulated effect reads all the fluents - unused_fluents.clear() - for f in a.simulated_effect.fluents: - static_fluents.discard(f.fluent()) elif isinstance(a, up.model.action.DurativeAction): for cl in a.conditions.values(): remove_used_fluents(*cl) @@ -350,8 +338,21 @@ def _get_static_and_unused_fluents( unused_fluents.clear() for f in se.fluents: static_fluents.discard(f.fluent()) - elif isinstance(a, up.model.natural_transition.Process): - for e in a.effects: + else: + raise NotImplementedError + for nt in self._natural_transitions: + if isinstance(nt, up.model.natural_transition.Event): + remove_used_fluents(*nt.preconditions) + for e in nt.effects: + remove_used_fluents(e.fluent, e.value, e.condition) + static_fluents.discard(e.fluent.fluent()) + if nt.simulated_effect is not None: + # empty the set because a simulated effect reads all the fluents + unused_fluents.clear() + for f in nt.simulated_effect.fluents: + static_fluents.discard(f.fluent()) + elif isinstance(nt, up.model.natural_transition.Process): + for e in nt.effects: remove_used_fluents(e.fluent, e.value, e.condition) static_fluents.discard(e.fluent.fluent()) else: From e66a811ceb9f0e6382c5f23f84db8f28a7c52d63 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 11:27:52 +0100 Subject: [PATCH 11/33] refactoring: some fixes, separated events and processes sets --- unified_planning/io/pddl_reader.py | 4 +- unified_planning/io/pddl_writer.py | 2 +- .../model/mixins/natural_transitions_set.py | 165 ++++++++++++------ unified_planning/model/problem.py | 37 ++-- unified_planning/test/examples/processes.py | 4 +- unified_planning/test/test_pddl_io.py | 19 +- 6 files changed, 144 insertions(+), 87 deletions(-) diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index d48ebf627..8c1de38a1 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -1292,7 +1292,7 @@ def declare_type( CustomParseResults(a["eff"][0]), domain_str, ) - problem.add_natural_transition(proc) + problem.add_process(proc) for a in domain_res.get("events", []): n = a["name"] @@ -1317,7 +1317,7 @@ def declare_type( CustomParseResults(a["eff"][0]), domain_str, ) - problem.add_natural_transition(evt) + problem.add_event(evt) for a in domain_res.get("actions", []): n = a["name"] diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 09fdd9bbb..db8e19ecf 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -534,6 +534,7 @@ def _write_domain(self, out: IO[str]): costs: Dict[ Union[up.model.NaturalTransition, up.model.Action], Optional[up.model.FNode] ] = {} + # TODO check if natural_transition should be here metrics = self.problem.quality_metrics if len(metrics) == 1: metric = metrics[0] @@ -703,7 +704,6 @@ def _write_domain(self, out: IO[str]): out.write(")\n") else: raise NotImplementedError - for nt in self.problem.natural_transitions: if isinstance(nt, up.model.Event): if any(p.simplify().is_false() for p in nt.preconditions): diff --git a/unified_planning/model/mixins/natural_transitions_set.py b/unified_planning/model/mixins/natural_transitions_set.py index 2f2e8bf1c..b2e22664b 100644 --- a/unified_planning/model/mixins/natural_transitions_set.py +++ b/unified_planning/model/mixins/natural_transitions_set.py @@ -32,9 +32,8 @@ def __init__(self, environment, add_user_type_method, has_name_method): self._env = environment self._add_user_type_method = add_user_type_method self._has_name_method = has_name_method - self._natural_transitions: List[ - "up.model.natural_transition.NaturalTransition" - ] = [] + self._events: List["up.model.natural_transition.Effect"] = [] + self._processes: List["up.model.natural_transition.Process"] = [] @property def environment(self) -> "up.environment.Environment": @@ -42,95 +41,151 @@ def environment(self) -> "up.environment.Environment": return self._env @property - def natural_transitions( + def processes( self, - ) -> List["up.model.natural_transition.NaturalTransition"]: - """Returns the list of the `NaturalTransitions` in the `Problem`.""" - return self._natural_transitions + ) -> List["up.model.natural_transition.Processes"]: + """Returns the list of the `Processes` in the `Problem`.""" + return self._processes - def clear_natural_transitions(self): - """Removes all the `Problem` `NaturalTransitions`.""" - self._natural_transitions = [] + @property + def events( + self, + ) -> List["up.model.natural_transition.Event"]: + """Returns the list of the `Events` in the `Problem`.""" + return self._events @property - def processes(self) -> Iterator["up.model.natural_transition.Process"]: - """Returs all the processes of the problem. + def natural_transitions( + self, + ) -> List["up.model.natural_transition.NaturalTransition"]: + """Returns the list of the `Processes` and `Events` in the `Problem`.""" + ntlist = [] + ntlist.extend(self._processes) + ntlist.extend(self._events) + return ntlist - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - for a in self._natural_transitions: - if isinstance(a, up.model.natural_transition.Process): - yield a + def clear_events(self): + """Removes all the `Problem` `Events`.""" + self._events = [] - @property - def events(self) -> Iterator["up.model.natural_transition.Event"]: - """Returs all the events of the problem. + def clear_processes(self): + """Removes all the `Problem` `Processes`.""" + self._processes = [] + + def process(self, name: str) -> "up.model.natural_transition.Process": + """ + Returns the `Process` with the given `name`. - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - for a in self._natural_transitions: - if isinstance(a, up.model.natural_transition.Event): - yield a + :param name: The `name` of the target `process`. + :return: The `process` in the `problem` with the given `name`. + """ + for a in self._processes: + if a.name == name: + return a + raise UPValueError(f"Process of name: {name} is not defined!") - def natural_transition( - self, name: str - ) -> "up.model.natural_transition.NaturalTransition": + def event(self, name: str) -> "up.model.natural_transition.Event": """ - Returns the `natural_transition` with the given `name`. + Returns the `event` with the given `name`. - :param name: The `name` of the target `natural_transition`. - :return: The `natural_transition` in the `problem` with the given `name`. + :param name: The `name` of the target `event`. + :return: The `event` in the `problem` with the given `name`. """ - for a in self._natural_transitions: + for a in self._event: if a.name == name: return a raise UPValueError(f"NaturalTransition of name: {name} is not defined!") - def has_natural_transition(self, name: str) -> bool: + def has_process(self, name: str) -> bool: """ - Returns `True` if the `problem` has the `natural_transition` with the given `name`, + Returns `True` if the `problem` has the `process` with the given `name`, `False` otherwise. - :param name: The `name` of the target `natural_transition`. - :return: `True` if the `problem` has an `natural_transition` with the given `name`, `False` otherwise. + :param name: The `name` of the target `process`. + :return: `True` if the `problem` has an `process` with the given `name`, `False` otherwise. """ - for a in self._natural_transitions: + for a in self._processes: if a.name == name: return True return False - def add_natural_transition( - self, natural_transition: "up.model.natural_transition.NaturalTransition" - ): + def has_event(self, name: str) -> bool: + """ + Returns `True` if the `problem` has the `event` with the given `name`, + `False` otherwise. + + :param name: The `name` of the target `event`. + :return: `True` if the `problem` has an `event` with the given `name`, `False` otherwise. + """ + for a in self._events: + if a.name == name: + return True + return False + + def add_process(self, process: "up.model.natural_transition.Process"): + """ + Adds the given `process` to the `problem`. + + :param natural_transition: The `process` that must be added to the `problem`. + """ + assert ( + process.environment == self._env + ), "Process does not have the same environment of the problem" + if self._has_name_method(process.name): + msg = f"Name {process.name} already defined! Different elements of a problem can have the same name if the environment flag error_used_name is disabled." + if self._env.error_used_name or any( + process.name == a.name for a in self._processes + ): + raise UPProblemDefinitionError(msg) + else: + warn(msg) + self._processes.append(process) + for param in process.parameters: + if param.type.is_user_type(): + self._add_user_type_method(param.type) + + def add_event(self, event: "up.model.natural_transition.Event"): """ - Adds the given `natural_transition` to the `problem`. + Adds the given `event` to the `problem`. - :param natural_transition: The `natural_transition` that must be added to the `problem`. + :param event: The `event` that must be added to the `problem`. """ assert ( - natural_transition.environment == self._env - ), "NaturalTransition does not have the same environment of the problem" - if self._has_name_method(natural_transition.name): - msg = f"Name {natural_transition.name} already defined! Different elements of a problem can have the same name if the environment flag error_used_name is disabled." + event.environment == self._env + ), "Event does not have the same environment of the problem" + if self._has_name_method(event.name): + msg = f"Name {event.name} already defined! Different elements of a problem can have the same name if the environment flag error_used_name is disabled." if self._env.error_used_name or any( - natural_transition.name == a.name for a in self._natural_transitions + event.name == a.name for a in self._events ): raise UPProblemDefinitionError(msg) else: warn(msg) - self._natural_transitions.append(natural_transition) - for param in natural_transition.parameters: + self._events.append(event) + for param in event.parameters: if param.type.is_user_type(): self._add_user_type_method(param.type) - def add_natural_transitions( + def add_processes( + self, + processes: Iterable["up.model.natural_transition.Process"], + ): + """ + Adds the given `processes` to the `problem`. + + :param processes: The `processes` that must be added to the `problem`. + """ + for process in processes: + self.add_process(process) + + def add_events( self, - natural_transitions: Iterable["up.model.natural_transition.NaturalTransition"], + events: Iterable["up.model.natural_transition.Event"], ): """ - Adds the given `natural_transitions` to the `problem`. + Adds the given `events` to the `problem`. - :param natural_transitions: The `natural_transitions` that must be added to the `problem`. + :param events: The `events` that must be added to the `problem`. """ - for natural_transition in natural_transitions: - self.add_natural_transition(natural_transition) + for event in events: + self.add_event(event) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index f5e7adcd9..1205a4aa1 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -122,6 +122,14 @@ def __repr__(self) -> str: s.append("actions = [\n") s.extend(map(custom_str, self.actions)) s.append("]\n\n") + if len(self.processes) > 0: + s.append("processes = [\n") + s.extend(map(custom_str, self.processes)) + s.append("]\n\n") + if len(self.events) > 0: + s.append("events = [\n") + s.extend(map(custom_str, self.events)) + s.append("]\n\n") if len(self.user_types) > 0: s.append("objects = [\n") for ty in self.user_types: @@ -240,7 +248,8 @@ def clone(self): TimeModelMixin._clone_to(self, new_p) new_p._actions = [a.clone() for a in self._actions] - new_p._natural_transitions = [a.clone() for a in self._natural_transitions] + new_p._events = [a.clone() for a in self._events] + new_p._processes = [a.clone() for a in self._processes] new_p._timed_effects = { t: [e.clone() for e in el] for t, el in self._timed_effects.items() } @@ -340,23 +349,15 @@ def _get_static_and_unused_fluents( static_fluents.discard(f.fluent()) else: raise NotImplementedError - for nt in self._natural_transitions: - if isinstance(nt, up.model.natural_transition.Event): - remove_used_fluents(*nt.preconditions) - for e in nt.effects: - remove_used_fluents(e.fluent, e.value, e.condition) - static_fluents.discard(e.fluent.fluent()) - if nt.simulated_effect is not None: - # empty the set because a simulated effect reads all the fluents - unused_fluents.clear() - for f in nt.simulated_effect.fluents: - static_fluents.discard(f.fluent()) - elif isinstance(nt, up.model.natural_transition.Process): - for e in nt.effects: - remove_used_fluents(e.fluent, e.value, e.condition) - static_fluents.discard(e.fluent.fluent()) - else: - raise NotImplementedError + for ev in self._events: + remove_used_fluents(*ev.preconditions) + for e in ev.effects: + remove_used_fluents(e.fluent, e.value, e.condition) + static_fluents.discard(e.fluent.fluent()) + for pro in self._processes: + for e in pro.effects: + remove_used_fluents(e.fluent, e.value, e.condition) + static_fluents.discard(e.fluent.fluent()) for el in self._timed_effects.values(): for e in el: remove_used_fluents(e.fluent, e.value, e.condition) diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 04e680621..70e95fbf4 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -25,8 +25,8 @@ def get_example_problems(): problem.add_fluent(on) problem.add_fluent(d) problem.add_action(a) - problem.add_natural_transition(b) - problem.add_natural_transition(evt) + problem.add_process(b) + problem.add_event(evt) problem.set_initial_value(on, False) problem.set_initial_value(d, 0) problem.add_goal(GE(d, 10)) diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 20ef0dd2e..f66c555db 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -442,16 +442,12 @@ def test_non_linear_car(self): self.assertTrue(problem is not None) self.assertEqual(len(problem.fluents), 8) - n_proc = len( - list([el for el in problem.natural_transitions if isinstance(el, Process)]) - ) - n_eve = len( - list([el for el in problem.natural_transitions if isinstance(el, Event)]) - ) + n_proc = len(list([el for el in problem.processes if isinstance(el, Process)])) + n_eve = len(list([el for el in problem.events if isinstance(el, Event)])) self.assertEqual(n_proc, 3) self.assertEqual(n_eve, 1) found_drag_ahead = False - for ele in problem.natural_transitions: + for ele in problem.processes: if isinstance(ele, Process): for e in ele.effects: self.assertEqual(e.kind, EffectKind.DERIVATIVE) @@ -581,6 +577,7 @@ def test_examples_io(self): w.write_problem(problem_filename) reader = PDDLReader() + print(problem) parsed_problem = reader.parse_problem(domain_filename, problem_filename) # Case where the reader does not convert the final_value back to actions_cost. @@ -598,8 +595,12 @@ def test_examples_io(self): ) self.assertEqual(len(problem.actions), len(parsed_problem.actions)) self.assertEqual( - len(problem.natural_transitions), - len(parsed_problem.natural_transitions), + len(problem.processes), + len(parsed_problem.processes), + ) + self.assertEqual( + len(problem.events), + len(parsed_problem.events), ) for a in problem.actions: parsed_a = parsed_problem.action(w.get_pddl_name(a)) From 1ec433c1b66a610f9a65fc59cbd6fe0f5b431911 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 11:32:18 +0100 Subject: [PATCH 12/33] refactoring: mypy --- .../model/mixins/natural_transitions_set.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/unified_planning/model/mixins/natural_transitions_set.py b/unified_planning/model/mixins/natural_transitions_set.py index b2e22664b..eaf7e8d45 100644 --- a/unified_planning/model/mixins/natural_transitions_set.py +++ b/unified_planning/model/mixins/natural_transitions_set.py @@ -16,7 +16,7 @@ from warnings import warn import unified_planning as up from unified_planning.exceptions import UPProblemDefinitionError, UPValueError -from typing import Iterator, List, Iterable +from typing import Iterator, List, Iterable, Union class NaturalTransitionsSetMixin: @@ -32,7 +32,7 @@ def __init__(self, environment, add_user_type_method, has_name_method): self._env = environment self._add_user_type_method = add_user_type_method self._has_name_method = has_name_method - self._events: List["up.model.natural_transition.Effect"] = [] + self._events: List["up.model.natural_transition.Event"] = [] self._processes: List["up.model.natural_transition.Process"] = [] @property @@ -43,7 +43,7 @@ def environment(self) -> "up.environment.Environment": @property def processes( self, - ) -> List["up.model.natural_transition.Processes"]: + ) -> List["up.model.natural_transition.Process"]: """Returns the list of the `Processes` in the `Problem`.""" return self._processes @@ -57,12 +57,13 @@ def events( @property def natural_transitions( self, - ) -> List["up.model.natural_transition.NaturalTransition"]: + ) -> List[ + Union[ + "up.model.natural_transition.Event", "up.model.natural_transition.Process" + ] + ]: """Returns the list of the `Processes` and `Events` in the `Problem`.""" - ntlist = [] - ntlist.extend(self._processes) - ntlist.extend(self._events) - return ntlist + return self.events + self.processes def clear_events(self): """Removes all the `Problem` `Events`.""" @@ -91,7 +92,7 @@ def event(self, name: str) -> "up.model.natural_transition.Event": :param name: The `name` of the target `event`. :return: The `event` in the `problem` with the given `name`. """ - for a in self._event: + for a in self._events: if a.name == name: return a raise UPValueError(f"NaturalTransition of name: {name} is not defined!") From c770833eaf625de7929061af72da09dea767e27f Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 11:43:37 +0100 Subject: [PATCH 13/33] refactoring: fixed natural_transition set property --- unified_planning/model/mixins/natural_transitions_set.py | 9 ++++++++- unified_planning/test/test_pddl_io.py | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/unified_planning/model/mixins/natural_transitions_set.py b/unified_planning/model/mixins/natural_transitions_set.py index eaf7e8d45..6259fd877 100644 --- a/unified_planning/model/mixins/natural_transitions_set.py +++ b/unified_planning/model/mixins/natural_transitions_set.py @@ -63,7 +63,14 @@ def natural_transitions( ] ]: """Returns the list of the `Processes` and `Events` in the `Problem`.""" - return self.events + self.processes + ntlist: List[ + Union[ + up.model.natural_transition.Event, up.model.natural_transition.Process + ] + ] = [] + ntlist.extend(self.processes) + ntlist.extend(self.events) + return ntlist def clear_events(self): """Removes all the `Problem` `Events`.""" diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index f66c555db..4e1c81bd0 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -577,7 +577,6 @@ def test_examples_io(self): w.write_problem(problem_filename) reader = PDDLReader() - print(problem) parsed_problem = reader.parse_problem(domain_filename, problem_filename) # Case where the reader does not convert the final_value back to actions_cost. From 11be2f1955f5622196b40eb2a7b4f1b12392d269 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 11:58:22 +0100 Subject: [PATCH 14/33] refactoring: removed simulated effects from processes and events --- unified_planning/model/natural_transition.py | 40 +------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 6c0a982df..c94a1b05f 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -29,8 +29,6 @@ from unified_planning.model.transition import Transition -# TODO check weather events/processes should have all the methods/other stuff that actions have. -# If yes, we probably need more tests for better coverage """ Below we have natural transitions. These are not controlled by the agent. Natural transitions can be of two kinds: @@ -57,7 +55,6 @@ def __init__( Transition.__init__(self, _name, _parameters, _env, **kwargs) self._preconditions: List["up.model.fnode.FNode"] = [] self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment self._fluents_assigned: Dict[ "up.model.fnode.FNode", "up.model.fnode.FNode" @@ -92,7 +89,6 @@ def __eq__(self, oth: object) -> bool: cond and set(self._preconditions) == set(oth._preconditions) and set(self._effects) == set(oth._effects) - and self._simulated_effect == oth._simulated_effect ) else: return False @@ -105,7 +101,6 @@ def __hash__(self) -> int: res += hash(p) for e in self._effects: res += hash(e) - res += hash(self._simulated_effect) return res def clone(self): @@ -117,7 +112,6 @@ def clone(self): new_process._effects = [e.clone() for e in self._effects] new_process._fluents_assigned = self._fluents_assigned.copy() new_process._fluents_inc_dec = self._fluents_inc_dec.copy() - new_process._simulated_effect = self._simulated_effect return new_process @property @@ -231,7 +225,6 @@ def __init__( Transition.__init__(self, _name, _parameters, _env, **kwargs) self._preconditions: List["up.model.fnode.FNode"] = [] self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment self._fluents_assigned: Dict[ "up.model.fnode.FNode", "up.model.fnode.FNode" @@ -250,7 +243,6 @@ def __eq__(self, oth: object) -> bool: cond and set(self._preconditions) == set(oth._preconditions) and set(self._effects) == set(oth._effects) - and self._simulated_effect == oth._simulated_effect ) else: return False @@ -263,7 +255,6 @@ def __hash__(self) -> int: res += hash(p) for e in self._effects: res += hash(e) - res += hash(self._simulated_effect) return res def clone(self): @@ -275,7 +266,6 @@ def clone(self): new_event._effects = [e.clone() for e in self._effects] new_event._fluents_assigned = self._fluents_assigned.copy() new_event._fluents_inc_dec = self._fluents_inc_dec.copy() - new_event._simulated_effect = self._simulated_effect return new_event @property @@ -297,7 +287,6 @@ def clear_effects(self): self._effects = [] self._fluents_assigned = {} self._fluents_inc_dec = set() - self._simulated_effect = None @property def conditional_effects(self) -> List["up.model.effect.Effect"]: @@ -485,37 +474,12 @@ def _add_effect_instance(self, effect: "up.model.effect.Effect"): up.model.effect.check_conflicting_effects( effect, None, - self._simulated_effect, - self._fluents_assigned, - self._fluents_inc_dec, - "event", - ) - self._effects.append(effect) - - @property - def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: - """Returns the `event` `simulated effect`.""" - return self._simulated_effect - - def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): - """ - Sets the given `simulated effect` as the only `event's simulated effect`. - - :param simulated_effect: The `SimulatedEffect` instance that must be set as this `event`'s only - `simulated effect`. - """ - up.model.effect.check_conflicting_simulated_effects( - simulated_effect, None, self._fluents_assigned, self._fluents_inc_dec, "event", ) - if simulated_effect.environment != self.environment: - raise UPUsageError( - "The added SimulatedEffect does not have the same environment of the Event" - ) - self._simulated_effect = simulated_effect + self._effects.append(effect) def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): self._preconditions = preconditions @@ -533,7 +497,5 @@ def __repr__(self) -> str: for e in self.effects: s.append(f" {str(e)}\n") s.append(" ]\n") - if self._simulated_effect is not None: - s.append(f" simulated effect = {self._simulated_effect}\n") s.append(" }") return "".join(s) From 5583f0423b0f9a8528d92e5b4cbdee8b7fe7a72c Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 14:00:01 +0100 Subject: [PATCH 15/33] refactoring: work in progress - mixin for InstantaneousAction, Event, Process --- unified_planning/model/action.py | 48 +---------- unified_planning/model/natural_transition.py | 89 +------------------- unified_planning/model/transition.py | 48 +++++++++++ 3 files changed, 55 insertions(+), 130 deletions(-) diff --git a/unified_planning/model/action.py b/unified_planning/model/action.py index 112e15800..094c9106e 100644 --- a/unified_planning/model/action.py +++ b/unified_planning/model/action.py @@ -30,7 +30,7 @@ from typing import Any, Dict, List, Set, Union, Optional, Iterable from collections import OrderedDict -from unified_planning.model.transition import Transition +from unified_planning.model.transition import SingleTimePointTransitionMixin, Transition class Action(Transition): @@ -50,7 +50,7 @@ def __call__( ) -class InstantaneousAction(Action): +class InstantaneousAction(Action, SingleTimePointTransitionMixin): """Represents an instantaneous action.""" def __init__( @@ -61,7 +61,7 @@ def __init__( **kwargs: "up.model.types.Type", ): Action.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] + SingleTimePointTransitionMixin.__init__(self, _env) self._effects: List[up.model.effect.Effect] = [] self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment @@ -139,15 +139,6 @@ def clone(self): new_instantaneous_action._simulated_effect = self._simulated_effect return new_instantaneous_action - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Action` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Action preconditions`""" - self._preconditions = [] - @property def effects(self) -> List["up.model.effect.Effect"]: """Returns the `list` of the `Action effects`.""" @@ -180,36 +171,6 @@ def unconditional_effects(self) -> List["up.model.effect.Effect"]: seldom as possible.""" return [e for e in self._effects if not e.is_conditional()] - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `action's preconditions`. - - :param precondition: The expression that must be added to the `action's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - def add_effect( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], @@ -378,9 +339,6 @@ def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffec ) self._simulated_effect = simulated_effect - def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): - self._preconditions = preconditions - class DurativeAction(Action, TimedCondsEffs): """Represents a durative action.""" diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index c94a1b05f..c888f800b 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -27,7 +27,7 @@ from typing import Any, Dict, List, Set, Union, Optional, Iterable from collections import OrderedDict -from unified_planning.model.transition import Transition +from unified_planning.model.transition import SingleTimePointTransitionMixin, Transition """ @@ -38,7 +38,7 @@ """ -class NaturalTransition(Transition): +class NaturalTransition(Transition, SingleTimePointTransitionMixin): """This is the `NaturalTransition` interface""" @@ -53,7 +53,7 @@ def __init__( **kwargs: "up.model.types.Type", ): Transition.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] + SingleTimePointTransitionMixin.__init__(self, _env) self._effects: List[up.model.effect.Effect] = [] # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment self._fluents_assigned: Dict[ @@ -114,15 +114,6 @@ def clone(self): new_process._fluents_inc_dec = self._fluents_inc_dec.copy() return new_process - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Process` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Process preconditions`""" - self._preconditions = [] - @property def effects(self) -> List["up.model.effect.Effect"]: """Returns the `list` of the `Process effects`.""" @@ -141,36 +132,6 @@ def _add_effect_instance(self, effect: "up.model.effect.Effect"): self._effects.append(effect) - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `Process's preconditions`. - - :param precondition: The expression that must be added to the `Process's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - def add_derivative( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], @@ -223,7 +184,7 @@ def __init__( **kwargs: "up.model.types.Type", ): Transition.__init__(self, _name, _parameters, _env, **kwargs) - self._preconditions: List["up.model.fnode.FNode"] = [] + SingleTimePointTransitionMixin.__init__(self, _env) self._effects: List[up.model.effect.Effect] = [] # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment self._fluents_assigned: Dict[ @@ -268,15 +229,6 @@ def clone(self): new_event._fluents_inc_dec = self._fluents_inc_dec.copy() return new_event - @property - def preconditions(self) -> List["up.model.fnode.FNode"]: - """Returns the `list` of the `Event` `preconditions`.""" - return self._preconditions - - def clear_preconditions(self): - """Removes all the `Event preconditions`""" - self._preconditions = [] - @property def effects(self) -> List["up.model.effect.Effect"]: """Returns the `list` of the `Event effects`.""" @@ -308,36 +260,6 @@ def unconditional_effects(self) -> List["up.model.effect.Effect"]: seldom as possible.""" return [e for e in self._effects if not e.is_conditional()] - def add_precondition( - self, - precondition: Union[ - "up.model.fnode.FNode", - "up.model.fluent.Fluent", - "up.model.parameter.Parameter", - bool, - ], - ): - """ - Adds the given expression to `event's preconditions`. - - :param precondition: The expression that must be added to the `event's preconditions`. - """ - (precondition_exp,) = self._environment.expression_manager.auto_promote( - precondition - ) - assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() - if precondition_exp == self._environment.expression_manager.TRUE(): - return - free_vars = self._environment.free_vars_oracle.get_free_variables( - precondition_exp - ) - if len(free_vars) != 0: - raise UPUnboundedVariablesError( - f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" - ) - if precondition_exp not in self._preconditions: - self._preconditions.append(precondition_exp) - def add_effect( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], @@ -481,9 +403,6 @@ def _add_effect_instance(self, effect: "up.model.effect.Effect"): ) self._effects.append(effect) - def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): - self._preconditions = preconditions - def __repr__(self) -> str: s = [] s.append(f"event {self.name}") diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py index f9d745ace..97c64cb17 100644 --- a/unified_planning/model/transition.py +++ b/unified_planning/model/transition.py @@ -152,3 +152,51 @@ def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": def is_conditional(self) -> bool: """Returns `True` if the `Transition` has `conditional effects`, `False` otherwise.""" raise NotImplementedError + + +class SingleTimePointTransitionMixin: + def __init__(self, _env): + self._preconditions: List["up.model.fnode.FNode"] = [] + self._environment = get_environment(_env) + + @property + def preconditions(self) -> List["up.model.fnode.FNode"]: + """Returns the `list` of the `Action` `preconditions`.""" + return self._preconditions + + def clear_preconditions(self): + """Removes all the `Action preconditions`""" + self._preconditions = [] + + def add_precondition( + self, + precondition: Union[ + "up.model.fnode.FNode", + "up.model.fluent.Fluent", + "up.model.parameter.Parameter", + bool, + ], + ): + """ + Adds the given expression to `action's preconditions`. + + :param precondition: The expression that must be added to the `action's preconditions`. + """ + (precondition_exp,) = self._environment.expression_manager.auto_promote( + precondition + ) + assert self._environment.type_checker.get_type(precondition_exp).is_bool_type() + if precondition_exp == self._environment.expression_manager.TRUE(): + return + free_vars = self._environment.free_vars_oracle.get_free_variables( + precondition_exp + ) + if len(free_vars) != 0: + raise UPUnboundedVariablesError( + f"The precondition {str(precondition_exp)} has unbounded variables:\n{str(free_vars)}" + ) + if precondition_exp not in self._preconditions: + self._preconditions.append(precondition_exp) + + def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): + self._preconditions = preconditions From 162427f79ed3e530eea5b86cdb3c3b20520ae754 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 3 Dec 2024 14:50:40 +0100 Subject: [PATCH 16/33] testing some functions --- unified_planning/model/problem.py | 4 ---- unified_planning/test/examples/processes.py | 6 ++++++ unified_planning/test/test_problem.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 1205a4aa1..592ebb0fc 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -1030,10 +1030,6 @@ def update_problem_kind_action( if len(action.simulated_effects) > 0: self.kind.set_simulated_entities("SIMULATED_EFFECTS") self.kind.set_time("CONTINUOUS_TIME") - elif isinstance(action, up.model.natural_transition.Process): - pass # TODO add Process kind - elif isinstance(action, up.model.natural_transition.Event): - pass # TODO add Event kind else: raise NotImplementedError diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 70e95fbf4..f4faec940 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -17,10 +17,16 @@ def get_example_problems(): evt.add_precondition(GE(d, 200)) evt.add_effect(on, False) + evt.clear_effects() + evt.add_effect(on, False) + b = Process("moving") b.add_precondition(on) b.add_derivative(d, 1) + b.clear_effects() + b.add_derivative(d, 1) + problem = Problem("1d_Movement") problem.add_fluent(on) problem.add_fluent(d) diff --git a/unified_planning/test/test_problem.py b/unified_planning/test/test_problem.py index 2189f311c..b59cf1bfb 100644 --- a/unified_planning/test/test_problem.py +++ b/unified_planning/test/test_problem.py @@ -606,6 +606,19 @@ def test_undefined_initial_state(self): pb_name, ) + def test_natural_transitions(self): + p = self.problems["1d_movement"].problem + print(p) + self.assertTrue(p.has_process("moving")) + self.assertTrue(p.has_event("turn_off_automatically")) + proc = p.process("moving") + evt = p.event("turn_off_automatically") + print(proc) + print(evt) + p.clear_events() + p.clear_processes() + self.assertEqual(len(p.natural_transitions), 0) + if __name__ == "__main__": main() From 8a8a44b9bfed54f52146810157e853ab57bdbdad Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Wed, 4 Dec 2024 11:14:16 +0100 Subject: [PATCH 17/33] refactoring: UntimedEffect and Precondition mixins --- unified_planning/model/action.py | 219 +---------------- .../model/mixins/natural_transitions_set.py | 12 +- unified_planning/model/natural_transition.py | 196 +-------------- unified_planning/model/transition.py | 223 +++++++++++++++++- 4 files changed, 238 insertions(+), 412 deletions(-) diff --git a/unified_planning/model/action.py b/unified_planning/model/action.py index 094c9106e..f01ed5396 100644 --- a/unified_planning/model/action.py +++ b/unified_planning/model/action.py @@ -30,7 +30,11 @@ from typing import Any, Dict, List, Set, Union, Optional, Iterable from collections import OrderedDict -from unified_planning.model.transition import SingleTimePointTransitionMixin, Transition +from unified_planning.model.transition import ( + UntimedEffectMixin, + PreconditionMixin, + Transition, +) class Action(Transition): @@ -50,7 +54,7 @@ def __call__( ) -class InstantaneousAction(Action, SingleTimePointTransitionMixin): +class InstantaneousAction(UntimedEffectMixin, Action, PreconditionMixin): """Represents an instantaneous action.""" def __init__( @@ -61,15 +65,8 @@ def __init__( **kwargs: "up.model.types.Type", ): Action.__init__(self, _name, _parameters, _env, **kwargs) - SingleTimePointTransitionMixin.__init__(self, _env) - self._effects: List[up.model.effect.Effect] = [] - self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None - # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment - self._fluents_assigned: Dict[ - "up.model.fnode.FNode", "up.model.fnode.FNode" - ] = {} - # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease - self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + PreconditionMixin.__init__(self, _env) + UntimedEffectMixin.__init__(self, _env) def __repr__(self) -> str: s = [] @@ -139,206 +136,6 @@ def clone(self): new_instantaneous_action._simulated_effect = self._simulated_effect return new_instantaneous_action - @property - def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Action effects`.""" - return self._effects - - def clear_effects(self): - """Removes all the `Action's effects`.""" - self._effects = [] - self._fluents_assigned = {} - self._fluents_inc_dec = set() - self._simulated_effect = None - - @property - def conditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action conditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if e.is_conditional()] - - def is_conditional(self) -> bool: - """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" - return any(e.is_conditional() for e in self._effects) - - @property - def unconditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `action unconditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if not e.is_conditional()] - - def add_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `assignment` to the `action's effects`. - - :param fluent: The `fluent` of which `value` is modified by the `assignment`. - :param value: The `value` to assign to the given `fluent`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." - ) - if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - # Value is not assignable to fluent (its type is not a subset of the fluent's type). - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - self._add_effect_instance( - up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) - ) - - def add_increase_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `increase effect` to the `action's effects`. - - :param fluent: The `fluent` which `value` is increased. - :param value: The given `fluent` is incremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote( - fluent, - value, - condition, - ) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Increase effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.INCREASE, - forall=forall, - ) - ) - - def add_decrease_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `decrease effect` to the `action's effects`. - - :param fluent: The `fluent` which value is decreased. - :param value: The given `fluent` is decremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Decrease effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DECREASE, - forall=forall, - ) - ) - - def _add_effect_instance(self, effect: "up.model.effect.Effect"): - assert ( - effect.environment == self._environment - ), "effect does not have the same environment of the action" - up.model.effect.check_conflicting_effects( - effect, - None, - self._simulated_effect, - self._fluents_assigned, - self._fluents_inc_dec, - "action", - ) - self._effects.append(effect) - - @property - def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: - """Returns the `action` `simulated effect`.""" - return self._simulated_effect - - def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): - """ - Sets the given `simulated effect` as the only `action's simulated effect`. - - :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only - `simulated effect`. - """ - up.model.effect.check_conflicting_simulated_effects( - simulated_effect, - None, - self._fluents_assigned, - self._fluents_inc_dec, - "action", - ) - if simulated_effect.environment != self.environment: - raise UPUsageError( - "The added SimulatedEffect does not have the same environment of the Action" - ) - self._simulated_effect = simulated_effect - class DurativeAction(Action, TimedCondsEffs): """Represents a durative action.""" diff --git a/unified_planning/model/mixins/natural_transitions_set.py b/unified_planning/model/mixins/natural_transitions_set.py index 6259fd877..e8cf24dfa 100644 --- a/unified_planning/model/mixins/natural_transitions_set.py +++ b/unified_planning/model/mixins/natural_transitions_set.py @@ -57,17 +57,9 @@ def events( @property def natural_transitions( self, - ) -> List[ - Union[ - "up.model.natural_transition.Event", "up.model.natural_transition.Process" - ] - ]: + ) -> List["up.model.natural_transition.NaturalTransition"]: """Returns the list of the `Processes` and `Events` in the `Problem`.""" - ntlist: List[ - Union[ - up.model.natural_transition.Event, up.model.natural_transition.Process - ] - ] = [] + ntlist: List[up.model.natural_transition.NaturalTransition] = [] ntlist.extend(self.processes) ntlist.extend(self.events) return ntlist diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index c888f800b..2b77618fe 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -27,7 +27,11 @@ from typing import Any, Dict, List, Set, Union, Optional, Iterable from collections import OrderedDict -from unified_planning.model.transition import SingleTimePointTransitionMixin, Transition +from unified_planning.model.transition import ( + UntimedEffectMixin, + PreconditionMixin, + Transition, +) """ @@ -38,7 +42,7 @@ """ -class NaturalTransition(Transition, SingleTimePointTransitionMixin): +class NaturalTransition(Transition, PreconditionMixin): """This is the `NaturalTransition` interface""" @@ -53,7 +57,7 @@ def __init__( **kwargs: "up.model.types.Type", ): Transition.__init__(self, _name, _parameters, _env, **kwargs) - SingleTimePointTransitionMixin.__init__(self, _env) + PreconditionMixin.__init__(self, _env) self._effects: List[up.model.effect.Effect] = [] # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment self._fluents_assigned: Dict[ @@ -173,7 +177,7 @@ def add_derivative( ) -class Event(NaturalTransition): +class Event(UntimedEffectMixin, NaturalTransition): """This class represents an event.""" def __init__( @@ -184,14 +188,8 @@ def __init__( **kwargs: "up.model.types.Type", ): Transition.__init__(self, _name, _parameters, _env, **kwargs) - SingleTimePointTransitionMixin.__init__(self, _env) - self._effects: List[up.model.effect.Effect] = [] - # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment - self._fluents_assigned: Dict[ - "up.model.fnode.FNode", "up.model.fnode.FNode" - ] = {} - # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease - self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + PreconditionMixin.__init__(self, _env) + UntimedEffectMixin.__init__(self, _env) def __eq__(self, oth: object) -> bool: if isinstance(oth, Event): @@ -229,180 +227,6 @@ def clone(self): new_event._fluents_inc_dec = self._fluents_inc_dec.copy() return new_event - @property - def effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `Event effects`.""" - return self._effects - - def clear_effects(self): - """Removes all the `Event's effects`.""" - self._effects = [] - self._fluents_assigned = {} - self._fluents_inc_dec = set() - - @property - def conditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `event conditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if e.is_conditional()] - - def is_conditional(self) -> bool: - """Returns `True` if the `event` has `conditional effects`, `False` otherwise.""" - return any(e.is_conditional() for e in self._effects) - - @property - def unconditional_effects(self) -> List["up.model.effect.Effect"]: - """Returns the `list` of the `event unconditional effects`. - - IMPORTANT NOTE: this property does some computation, so it should be called as - seldom as possible.""" - return [e for e in self._effects if not e.is_conditional()] - - def add_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `assignment` to the `event's effects`. - - :param fluent: The `fluent` of which `value` is modified by the `assignment`. - :param value: The `value` to assign to the given `fluent`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." - ) - if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - # Value is not assignable to fluent (its type is not a subset of the fluent's type). - raise UPTypeError( - f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - self._add_effect_instance( - up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) - ) - - def add_increase_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `increase effect` to the `event's effects`. - - :param fluent: The `fluent` which `value` is increased. - :param value: The given `fluent` is incremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote( - fluent, - value, - condition, - ) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Increase effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.INCREASE, - forall=forall, - ) - ) - - def add_decrease_effect( - self, - fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], - value: "up.model.expression.Expression", - condition: "up.model.expression.BoolExpression" = True, - forall: Iterable["up.model.variable.Variable"] = tuple(), - ): - """ - Adds the given `decrease effect` to the `event's effects`. - - :param fluent: The `fluent` which value is decreased. - :param value: The given `fluent` is decremented by the given `value`. - :param condition: The `condition` in which this `effect` is applied; the default - value is `True`. - :param forall: The 'Variables' that are universally quantified in this - effect; the default value is empty. - """ - ( - fluent_exp, - value_exp, - condition_exp, - ) = self._environment.expression_manager.auto_promote(fluent, value, condition) - if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): - raise UPUsageError( - "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." - ) - if not condition_exp.type.is_bool_type(): - raise UPTypeError("Effect condition is not a Boolean condition!") - if not fluent_exp.type.is_compatible(value_exp.type): - raise UPTypeError( - f"Event effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" - ) - if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): - raise UPTypeError("Decrease effects can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DECREASE, - forall=forall, - ) - ) - - def _add_effect_instance(self, effect: "up.model.effect.Effect"): - assert ( - effect.environment == self._environment - ), "effect does not have the same environment of the event" - up.model.effect.check_conflicting_effects( - effect, - None, - None, - self._fluents_assigned, - self._fluents_inc_dec, - "event", - ) - self._effects.append(effect) - def __repr__(self) -> str: s = [] s.append(f"event {self.name}") diff --git a/unified_planning/model/transition.py b/unified_planning/model/transition.py index 97c64cb17..deb26ee29 100644 --- a/unified_planning/model/transition.py +++ b/unified_planning/model/transition.py @@ -90,6 +90,10 @@ def __hash__(self) -> int: def clone(self): raise NotImplementedError + def is_conditional(self) -> bool: + """Returns `True` if the `Transition` has `conditional effects`, `False` otherwise.""" + raise NotImplementedError + @property def environment(self) -> Environment: """Returns this `Transition` `Environment`.""" @@ -149,12 +153,8 @@ def __getattr__(self, parameter_name: str) -> "up.model.parameter.Parameter": ) return self._parameters[parameter_name] - def is_conditional(self) -> bool: - """Returns `True` if the `Transition` has `conditional effects`, `False` otherwise.""" - raise NotImplementedError - -class SingleTimePointTransitionMixin: +class PreconditionMixin: def __init__(self, _env): self._preconditions: List["up.model.fnode.FNode"] = [] self._environment = get_environment(_env) @@ -200,3 +200,216 @@ def add_precondition( def _set_preconditions(self, preconditions: List["up.model.fnode.FNode"]): self._preconditions = preconditions + + +class UntimedEffectMixin: + def __init__(self, _env): + self._environment = get_environment(_env) + self._effects: List[up.model.effect.Effect] = [] + self._simulated_effect: Optional[up.model.effect.SimulatedEffect] = None + # fluent assigned is the mapping of the fluent to it's value if it is an unconditional assignment + self._fluents_assigned: Dict[ + "up.model.fnode.FNode", "up.model.fnode.FNode" + ] = {} + # fluent_inc_dec is the set of the fluents that have an unconditional increase or decrease + self._fluents_inc_dec: Set["up.model.fnode.FNode"] = set() + + @property + def effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `Action effects`.""" + return self._effects + + def clear_effects(self): + """Removes all the `Action's effects`.""" + self._effects = [] + self._fluents_assigned = {} + self._fluents_inc_dec = set() + self._simulated_effect = None + + @property + def conditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action conditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if e.is_conditional()] + + def is_conditional(self) -> bool: + """Returns `True` if the `action` has `conditional effects`, `False` otherwise.""" + return any(e.is_conditional() for e in self._effects) + + @property + def unconditional_effects(self) -> List["up.model.effect.Effect"]: + """Returns the `list` of the `action unconditional effects`. + + IMPORTANT NOTE: this property does some computation, so it should be called as + seldom as possible.""" + return [e for e in self._effects if not e.is_conditional()] + + def add_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `assignment` to the `action's effects`. + + :param fluent: The `fluent` of which `value` is modified by the `assignment`. + :param value: The `value` to assign to the given `fluent`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_effect must be a Fluent or a FluentExp or a Dot." + ) + if not self._environment.type_checker.get_type(condition_exp).is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + # Value is not assignable to fluent (its type is not a subset of the fluent's type). + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + self._add_effect_instance( + up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall) + ) + + def add_increase_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `increase effect` to the `action's effects`. + + :param fluent: The `fluent` which `value` is increased. + :param value: The given `fluent` is incremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote( + fluent, + value, + condition, + ) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_increase_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Increase effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.INCREASE, + forall=forall, + ) + ) + + def add_decrease_effect( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + condition: "up.model.expression.BoolExpression" = True, + forall: Iterable["up.model.variable.Variable"] = tuple(), + ): + """ + Adds the given `decrease effect` to the `action's effects`. + + :param fluent: The `fluent` which value is decreased. + :param value: The given `fluent` is decremented by the given `value`. + :param condition: The `condition` in which this `effect` is applied; the default + value is `True`. + :param forall: The 'Variables' that are universally quantified in this + effect; the default value is empty. + """ + ( + fluent_exp, + value_exp, + condition_exp, + ) = self._environment.expression_manager.auto_promote(fluent, value, condition) + if not fluent_exp.is_fluent_exp() and not fluent_exp.is_dot(): + raise UPUsageError( + "fluent field of add_decrease_effect must be a Fluent or a FluentExp or a Dot." + ) + if not condition_exp.type.is_bool_type(): + raise UPTypeError("Effect condition is not a Boolean condition!") + if not fluent_exp.type.is_compatible(value_exp.type): + raise UPTypeError( + f"InstantaneousAction effect has an incompatible value type. Fluent type: {fluent_exp.type} // Value type: {value_exp.type}" + ) + if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): + raise UPTypeError("Decrease effects can be created only on numeric types!") + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.DECREASE, + forall=forall, + ) + ) + + def _add_effect_instance(self, effect: "up.model.effect.Effect"): + assert ( + effect.environment == self._environment + ), "effect does not have the same environment of the action" + up.model.effect.check_conflicting_effects( + effect, + None, + self._simulated_effect, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + self._effects.append(effect) + + @property + def simulated_effect(self) -> Optional["up.model.effect.SimulatedEffect"]: + """Returns the `action` `simulated effect`.""" + return self._simulated_effect + + def set_simulated_effect(self, simulated_effect: "up.model.effect.SimulatedEffect"): + """ + Sets the given `simulated effect` as the only `action's simulated effect`. + + :param simulated_effect: The `SimulatedEffect` instance that must be set as this `action`'s only + `simulated effect`. + """ + up.model.effect.check_conflicting_simulated_effects( + simulated_effect, + None, + self._fluents_assigned, + self._fluents_inc_dec, + "action", + ) + if simulated_effect.environment != self._environment: + raise UPUsageError( + "The added SimulatedEffect does not have the same environment of the Action" + ) + self._simulated_effect = simulated_effect From 7bd6be1b12a4ec4e7c2dc0e097c510dd4a505e50 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Thu, 5 Dec 2024 08:58:16 +0100 Subject: [PATCH 18/33] refactoring: separated events and processes in pddl writer --- unified_planning/io/pddl_writer.py | 127 +++++++++++++------------- unified_planning/test/test_pddl_io.py | 2 +- 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index db8e19ecf..aba332267 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -704,73 +704,72 @@ def _write_domain(self, out: IO[str]): out.write(")\n") else: raise NotImplementedError - for nt in self.problem.natural_transitions: - if isinstance(nt, up.model.Event): - if any(p.simplify().is_false() for p in nt.preconditions): - continue - out.write(f" (:event {self._get_mangled_name(nt)}") - out.write(f"\n :parameters (") - self._write_parameters(out, nt) + for proc in self.problem.processes: + + if any(p.simplify().is_false() for p in proc.preconditions): + continue + out.write(f" (:process {self._get_mangled_name(proc)}") + out.write(f"\n :parameters (") + self._write_parameters(out, proc) + out.write(")") + if len(proc.preconditions) > 0: + precond_str = [] + for p in (c.simplify() for c in proc.preconditions): + if not p.is_true(): + if p.is_and(): + precond_str.extend(map(converter.convert, p.args)) + else: + precond_str.append(converter.convert(p)) + out.write(f'\n :precondition (and {" ".join(precond_str)})') + elif len(proc.preconditions) == 0 and self.empty_preconditions: + out.write(f"\n :precondition ()") + if len(proc.effects) > 0: + out.write("\n :effect (and") + for e in proc.effects: + _write_derivative( + e, + out, + converter, + ) out.write(")") - if len(nt.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in nt.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(nt.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") - if len(nt.effects) > 0: - out.write("\n :effect (and") - for e in nt.effects: - _write_effect( - e, - None, - out, - converter, - self.rewrite_bool_assignments, - self._get_mangled_name, - ) + out.write(")\n") + for eve in self.problem.events: - if nt in costs: - out.write( - f" (increase (total-cost) {converter.convert(costs[nt])})" - ) - out.write(")") - out.write(")\n") - elif isinstance(nt, up.model.Process): - if any(p.simplify().is_false() for p in nt.preconditions): - continue - out.write(f" (:process {self._get_mangled_name(nt)}") - out.write(f"\n :parameters (") - self._write_parameters(out, nt) + if any(p.simplify().is_false() for p in eve.preconditions): + continue + out.write(f" (:event {self._get_mangled_name(eve)}") + out.write(f"\n :parameters (") + self._write_parameters(out, eve) + out.write(")") + if len(eve.preconditions) > 0: + precond_str = [] + for p in (c.simplify() for c in eve.preconditions): + if not p.is_true(): + if p.is_and(): + precond_str.extend(map(converter.convert, p.args)) + else: + precond_str.append(converter.convert(p)) + out.write(f'\n :precondition (and {" ".join(precond_str)})') + elif len(eve.preconditions) == 0 and self.empty_preconditions: + out.write(f"\n :precondition ()") + if len(eve.effects) > 0: + out.write("\n :effect (and") + for e in eve.effects: + _write_effect( + e, + None, + out, + converter, + self.rewrite_bool_assignments, + self._get_mangled_name, + ) + + if eve in costs: + out.write( + f" (increase (total-cost) {converter.convert(costs[eve])})" + ) out.write(")") - if len(nt.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in nt.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(nt.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") - if len(nt.effects) > 0: - out.write("\n :effect (and") - for e in nt.effects: - _write_derivative( - e, - out, - converter, - ) - out.write(")") - out.write(")\n") - else: - raise NotImplementedError + out.write(")\n") out.write(")\n") def _write_problem(self, out: IO[str]): diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 4e1c81bd0..7d2ccc9d0 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -14,7 +14,7 @@ import os import tempfile -import pytest # type: ignore +import pytest from typing import cast import unified_planning from unified_planning.shortcuts import * From 5238ea581763fa1e7935858b57839b6992b5644c Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Thu, 5 Dec 2024 11:02:20 +0100 Subject: [PATCH 19/33] new test case in processes examples --- unified_planning/test/examples/processes.py | 58 ++++++++++++++++++--- unified_planning/test/test_problem.py | 1 + 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index f4faec940..fffdb35f7 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -17,16 +17,10 @@ def get_example_problems(): evt.add_precondition(GE(d, 200)) evt.add_effect(on, False) - evt.clear_effects() - evt.add_effect(on, False) - b = Process("moving") b.add_precondition(on) b.add_derivative(d, 1) - b.clear_effects() - b.add_derivative(d, 1) - problem = Problem("1d_Movement") problem.add_fluent(on) problem.add_fluent(d) @@ -47,4 +41,56 @@ def get_example_problems(): invalid_plans=[], ) problems["1d_movement"] = test_problem + + problem = Problem("boiling_water") + boiler_on = Fluent("boiler_on") + temperature = Fluent("temperature", RealType()) + water_level = Fluent("water_level", RealType()) + chimney_vent_open = Fluent("chimney_vent_open") + + turn_on_boiler = InstantaneousAction("turn_on_boiler") + turn_on_boiler.add_precondition(Not(boiler_on)) + turn_on_boiler.add_effect(boiler_on, True) + + water_heating = Process("water_heating") + water_heating.add_precondition(And(boiler_on, LE(temperature, 100))) + water_heating.add_derivative(temperature, 1) + + water_boiling = Process("water_boiling") + water_boiling.add_precondition(And(boiler_on, GE(temperature, 100))) + water_boiling.add_derivative(water_level, -1) + + open_chimney_vent_auto = Event("open_chimney_vent_auto") + open_chimney_vent_auto.add_precondition( + And(Not(chimney_vent_open), GE(temperature, 100)) + ) + open_chimney_vent_auto.add_effect(chimney_vent_open, True) + + turn_off_boiler_auto = Event("turn_off_boiler_auto") + turn_off_boiler_auto.add_precondition(And(LE(water_level, 0), boiler_on)) + turn_off_boiler_auto.add_effect(boiler_on, False) + + problem.add_fluent(boiler_on) + problem.set_initial_value(boiler_on, False) + problem.add_fluent(chimney_vent_open) + problem.set_initial_value(chimney_vent_open, False) + problem.add_fluent(temperature) + problem.set_initial_value(temperature, 20) + problem.add_fluent(water_level) + problem.set_initial_value(water_level, 10) + problem.add_action(turn_on_boiler) + problem.add_process(water_heating) + problem.add_process(water_boiling) + problem.add_event(open_chimney_vent_auto) + problem.add_event(turn_off_boiler_auto) + problem.add_goal(And(Not(boiler_on), And(chimney_vent_open, LE(water_level, 2)))) + + test_problem = TestCase( + problem=problem, + solvable=True, + valid_plans=[], + invalid_plans=[], + ) + problems["boiling_water"] = test_problem + return problems diff --git a/unified_planning/test/test_problem.py b/unified_planning/test/test_problem.py index b59cf1bfb..1cce81f4c 100644 --- a/unified_planning/test/test_problem.py +++ b/unified_planning/test/test_problem.py @@ -531,6 +531,7 @@ def test_simple_numeric_planning_kind(self): "sched:resource_set", "sched:jobshop-ft06-operators", "1d_Movement", + "boiling_water", ] for example in self.problems.values(): problem = example.problem From 5ffee71cb81a8badab9cc26f6a7dd6db97fe2ed8 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 13 Dec 2024 08:54:35 +0100 Subject: [PATCH 20/33] refactoring: removed duplicate code in pddl writer --- unified_planning/io/pddl_writer.py | 102 ++++++++++------------------- 1 file changed, 35 insertions(+), 67 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index aba332267..221cdaf00 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -606,34 +606,8 @@ def _write_domain(self, out: IO[str]): out.write(f"\n :parameters (") self._write_parameters(out, a) out.write(")") - if len(a.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in a.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(a.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") - if len(a.effects) > 0: - out.write("\n :effect (and") - for e in a.effects: - _write_effect( - e, - None, - out, - converter, - self.rewrite_bool_assignments, - self._get_mangled_name, - ) - - if a in costs: - out.write( - f" (increase (total-cost) {converter.convert(costs[a])})" - ) - out.write(")") + self._write_untimed_preconditions(a, converter, out) + self._write_untimed_effects(a, converter, out, costs) out.write(")\n") elif isinstance(a, DurativeAction): if any( @@ -712,17 +686,7 @@ def _write_domain(self, out: IO[str]): out.write(f"\n :parameters (") self._write_parameters(out, proc) out.write(")") - if len(proc.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in proc.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(proc.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") + self._write_untimed_preconditions(proc, converter, out) if len(proc.effects) > 0: out.write("\n :effect (and") for e in proc.effects: @@ -741,34 +705,8 @@ def _write_domain(self, out: IO[str]): out.write(f"\n :parameters (") self._write_parameters(out, eve) out.write(")") - if len(eve.preconditions) > 0: - precond_str = [] - for p in (c.simplify() for c in eve.preconditions): - if not p.is_true(): - if p.is_and(): - precond_str.extend(map(converter.convert, p.args)) - else: - precond_str.append(converter.convert(p)) - out.write(f'\n :precondition (and {" ".join(precond_str)})') - elif len(eve.preconditions) == 0 and self.empty_preconditions: - out.write(f"\n :precondition ()") - if len(eve.effects) > 0: - out.write("\n :effect (and") - for e in eve.effects: - _write_effect( - e, - None, - out, - converter, - self.rewrite_bool_assignments, - self._get_mangled_name, - ) - - if eve in costs: - out.write( - f" (increase (total-cost) {converter.convert(costs[eve])})" - ) - out.write(")") + self._write_untimed_preconditions(eve, converter, out) + self._write_untimed_effects(eve, converter, out, costs) out.write(")\n") out.write(")\n") @@ -1103,6 +1041,36 @@ def format_subtask(t: up.model.htn.Subtask): "Task network constraints not supported by HDDL Writer yet" ) + def _write_untimed_preconditions(self, item, converter, out): + if len(item.preconditions) > 0: + precond_str: list[str] = [] + for p in (c.simplify() for c in item.preconditions): + if not p.is_true(): + if p.is_and(): + precond_str.extend(map(converter.convert, p.args)) + else: + precond_str.append(converter.convert(p)) + out.write(f'\n :precondition (and {" ".join(precond_str)})') + elif len(item.preconditions) == 0 and self.empty_preconditions: + out.write(f"\n :precondition ()") + + def _write_untimed_effects(self, item, converter, out, costs): + if len(item.effects) > 0: + out.write("\n :effect (and") + for e in item.effects: + _write_effect( + e, + None, + out, + converter, + self.rewrite_bool_assignments, + self._get_mangled_name, + ) + + if item in costs: + out.write(f" (increase (total-cost) {converter.convert(costs[item])})") + out.write(")") + def _get_pddl_name(item: Union[WithName, "up.model.AbstractProblem"]) -> str: """This function returns a pddl name for the chosen item""" From e83b3261226ed1e3584bfe6a317dd82ad40a3f47 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 11:02:39 +0100 Subject: [PATCH 21/33] updated kinds and documentation, separated functions for increase/decrease --- docs/problem_representation.rst | 15 +++++ unified_planning/io/pddl_reader.py | 6 +- unified_planning/io/pddl_writer.py | 2 +- unified_planning/model/effect.py | 20 +++--- unified_planning/model/natural_transition.py | 64 +++++++++++++++----- unified_planning/model/problem.py | 20 ++++++ unified_planning/model/problem_kind.py | 4 ++ unified_planning/test/examples/processes.py | 6 +- unified_planning/test/test_model.py | 7 ++- unified_planning/test/test_pddl_io.py | 5 +- 10 files changed, 117 insertions(+), 32 deletions(-) diff --git a/docs/problem_representation.rst b/docs/problem_representation.rst index 61f155c7a..24194d66e 100644 --- a/docs/problem_representation.rst +++ b/docs/problem_representation.rst @@ -63,6 +63,12 @@ Problem Kinds * - - SELF_OVERLAPPING - The temporal planning problem allows actions self overlapping. + * - + - PROCESSES + - The problem contains processes, natural transitions that occur automatically over time as their pre-conditions are met. + * - + - EVENTS + - The problem contains events, natural transitions that occur automatically at a single time point when their pre-conditions are met. * - EXPRESSION_DURATION - STATIC_FLUENTS_IN_DURATION - The duration of at least one action uses static fluents (that may never change). @@ -111,6 +117,15 @@ Problem Kinds * - - DECREASE_EFFECTS - At least one effect uses the numeric decrement operator. + * - + - INCREASE_CONTINUOUS_EFFECTS + - At least one effect uses the continuous numeric increment operator. + * - + - DECREASE_CONTINUOUS_EFFECTS + - At least one effect uses the continuous numeric decrement operator. + * - + - NON_LINEAR_CONTINUOUS_EFFECTS + - At least one continuous effect is described by a differential equation that depends on a continuous variable. * - - STATIC_FLUENTS_IN_BOOLEAN_ASSIGNMENTS - At least one effect uses a static fluent in the expression of a boolean assignment. diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index 8c1de38a1..5e75ede5b 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -655,7 +655,7 @@ def _add_effect( if isinstance(act, up.model.Process): eff1 = (eff[0], eff[1].simplify()) - act.add_derivative(*eff1) + act.add_continuous_increase(*eff1) else: act.add_increase_effect(*eff if timing is None else (timing, *eff)) # type: ignore elif op == "decrease": @@ -669,8 +669,8 @@ def _add_effect( cond, ) if isinstance(act, up.model.Process): - eff1 = (eff[0], (eff[1] * (-1)).simplify()) - act.add_derivative(*eff1) + eff1 = (eff[0], eff[1].simplify()) + act.add_continuous_decrease(*eff1) else: act.add_decrease_effect(*eff if timing is None else (timing, *eff)) # type: ignore elif op == "forall": diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 221cdaf00..bc29d514f 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -1240,7 +1240,7 @@ def _write_derivative( out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") elif effect.is_decrease(): out.write(f" (decrease {fluent} (* #t {converter.convert(simplified_value)} ))") - elif effect.is_derivative(): + elif effect.is_continuous_increase() or effect.is_continuous_decrease(): out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") else: raise UPProblemDefinitionError( diff --git a/unified_planning/model/effect.py b/unified_planning/model/effect.py index 1a7d8f240..236d50f72 100644 --- a/unified_planning/model/effect.py +++ b/unified_planning/model/effect.py @@ -38,13 +38,15 @@ class EffectKind(Enum): `ASSIGN` => `if C then F <= V` `INCREASE` => `if C then F <= F + V` `DECREASE` => `if C then F <= F - V` - `DERIVATIVE` => `dF/dt <= V` + `CONTINUOUS_INCREASE` => `dF/dt <= V` + `CONTINUOUS_DECREASE` => `dF/dt <= -V` """ ASSIGN = auto() INCREASE = auto() DECREASE = auto() - DERIVATIVE = auto() + CONTINUOUS_INCREASE = auto() + CONTINUOUS_DECREASE = auto() class Effect: @@ -111,7 +113,7 @@ def __repr__(self) -> str: s.append(f"forall {', '.join(str(v) for v in self._forall)}") if self.is_conditional(): s.append(f"if {str(self._condition)} then") - if not (self.is_derivative()): + if not (self.is_continuous_increase() or self.is_continuous_decrease()): s.append(f"{str(self._fluent)}") if self.is_assignment(): s.append(":=") @@ -119,7 +121,7 @@ def __repr__(self) -> str: s.append("+=") elif self.is_decrease(): s.append("-=") - elif self.is_derivative(): + elif self.is_continuous_increase() or self.is_continuous_decrease(): s.append(f"d{str(self._fluent)}/dt =") s.append(f"{str(self._value)}") return " ".join(s) @@ -250,9 +252,13 @@ def is_decrease(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `decrease`, `False` otherwise.""" return self._kind == EffectKind.DECREASE - def is_derivative(self) -> bool: - """Returns `True` if the :func:`kind ` of this `Effect` is a `derivative`, `False` otherwise.""" - return self._kind == EffectKind.DERIVATIVE + def is_continuous_increase(self) -> bool: + """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous increase`, `False` otherwise.""" + return self._kind == EffectKind.CONTINUOUS_INCREASE + + def is_continuous_decrease(self) -> bool: + """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous decrease`, `False` otherwise.""" + return self._kind == EffectKind.CONTINUOUS_DECREASE class SimulatedEffect: diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 2b77618fe..bd6120bf6 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -136,17 +136,12 @@ def _add_effect_instance(self, effect: "up.model.effect.Effect"): self._effects.append(effect) - def add_derivative( + def _add_continuous_effect( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], value: "up.model.expression.Expression", + negative: bool, ): - """ - Adds the given `time derivative effect` to the `process's effects`. - - :param fluent: The `fluent` is the numeric state variable of which this process expresses its time derivative, which in Newton's notation would be over-dot(fluent). - :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. - """ ( fluent_exp, value_exp, @@ -166,15 +161,54 @@ def add_derivative( ) if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): raise UPTypeError("Derivative can be created only on numeric types!") - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.DERIVATIVE, - forall=tuple(), + if not negative: + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.CONTINUOUS_INCREASE, + forall=tuple(), + ) ) - ) + else: + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=up.model.effect.EffectKind.CONTINUOUS_DECREASE, + forall=tuple(), + ) + ) + + def add_continuous_increase( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + ): + """ + Adds the given `continuous increase effect` to the `process's effects`. + + NOTE description might be outdated + :param fluent: The `fluent` is the numeric state variable of which this process expresses its continuous increase, which in Newton's notation would be over-dot(fluent). + :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. + """ + self._add_continuous_effect(fluent, value, False) + + def add_continuous_decrease( + self, + fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], + value: "up.model.expression.Expression", + ): + """ + Adds the given `continuous decrease effect` to the `process's effects`. + + NOTE description might be outdated + :param fluent: The `fluent` is the numeric state variable of which this process expresses its continuous decrease, which in Newton's notation would be over-dot(fluent). + :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. + """ + self._add_continuous_effect(fluent, value, True) class Event(UntimedEffectMixin, NaturalTransition): diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 592ebb0fc..e8b7f802e 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -709,6 +709,8 @@ def _kind_factory(self) -> "_KindFactory": factory.update_problem_kind_initial_state(self) if len(list(self.processes)) > 0: factory.kind.set_time("PROCESSES") + if len(list(self.events)) > 0: + factory.kind.set_time("EVENTS") return factory @@ -884,6 +886,10 @@ def update_problem_kind_effect( self.kind.set_effects_kind("STATIC_FLUENTS_IN_OBJECT_ASSIGNMENTS") if any(f not in self.static_fluents for f in fluents_in_value): self.kind.set_effects_kind("FLUENTS_IN_OBJECT_ASSIGNMENTS") + elif e.is_continuous_increase(): + self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") + elif e.is_continuous_decrease(): + self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") def update_problem_kind_expression( self, @@ -1027,6 +1033,20 @@ def update_problem_kind_action( for t, le in action.effects.items(): for e in le: self.update_action_timed_effect(t, e) + continuous_fluents = set() + fluents_in_rhs = set() + # TODO + for t, le in action.effects.items(): + for e in le: + self.update_action_timed_effect(t, e) + continuous_fluents.add(e.fluent.fluent) + rhs = self.simplifier.simplify(e.value) + for var in self.environment.free_vars_extractor.get(rhs): + if var.is_fluent_exp(): + fluents_in_rhs.add(var.fluent) + if any(variable in fluents_in_rhs for variable in continuous_fluents): + self.kind.set_effects_kind("NON_LINEAR_CONTINUOUS_EFFECTS") + if len(action.simulated_effects) > 0: self.kind.set_simulated_entities("SIMULATED_EFFECTS") self.kind.set_time("CONTINUOUS_TIME") diff --git a/unified_planning/model/problem_kind.py b/unified_planning/model/problem_kind.py index 52ae19d9e..91482f570 100644 --- a/unified_planning/model/problem_kind.py +++ b/unified_planning/model/problem_kind.py @@ -47,6 +47,7 @@ "DURATION_INEQUALITIES", "SELF_OVERLAPPING", "PROCESSES", + "EVENTS", ], "EXPRESSION_DURATION": [ "STATIC_FLUENTS_IN_DURATIONS", @@ -70,6 +71,9 @@ "CONDITIONAL_EFFECTS", "INCREASE_EFFECTS", "DECREASE_EFFECTS", + "INCREASE_CONTINUOUS_EFFECTS", + "DECREASE_CONTINUOUS_EFFECTS", + "NON_LINEAR_CONTINUOUS_EFFECTS", "STATIC_FLUENTS_IN_BOOLEAN_ASSIGNMENTS", "STATIC_FLUENTS_IN_NUMERIC_ASSIGNMENTS", "STATIC_FLUENTS_IN_OBJECT_ASSIGNMENTS", diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index fffdb35f7..cd0f4fd9e 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -19,7 +19,7 @@ def get_example_problems(): b = Process("moving") b.add_precondition(on) - b.add_derivative(d, 1) + b.add_continuous_increase(d, 1) problem = Problem("1d_Movement") problem.add_fluent(on) @@ -54,11 +54,11 @@ def get_example_problems(): water_heating = Process("water_heating") water_heating.add_precondition(And(boiler_on, LE(temperature, 100))) - water_heating.add_derivative(temperature, 1) + water_heating.add_continuous_increase(temperature, 1) water_boiling = Process("water_boiling") water_boiling.add_precondition(And(boiler_on, GE(temperature, 100))) - water_boiling.add_derivative(water_level, -1) + water_boiling.add_continuous_decrease(water_level, 1) open_chimney_vent_auto = Event("open_chimney_vent_auto") open_chimney_vent_auto.add_precondition( diff --git a/unified_planning/test/test_model.py b/unified_planning/test/test_model.py index 14ac7d60f..2c63e9f6e 100644 --- a/unified_planning/test/test_model.py +++ b/unified_planning/test/test_model.py @@ -126,9 +126,12 @@ def test_process(self): x = Fluent("x", IntType()) move = Process("moving", car=Vehicle) move.add_precondition(a) - move.add_derivative(x, 1) + move.add_continuous_increase(x, 1) e = Effect( - FluentExp(x), Int(1), TRUE(), unified_planning.model.EffectKind.DERIVATIVE + FluentExp(x), + Int(1), + TRUE(), + unified_planning.model.EffectKind.CONTINUOUS_INCREASE, ) self.assertEqual(move.effects[0], e) self.assertEqual(move.name, "moving") diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 7d2ccc9d0..71fe65539 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -450,7 +450,10 @@ def test_non_linear_car(self): for ele in problem.processes: if isinstance(ele, Process): for e in ele.effects: - self.assertEqual(e.kind, EffectKind.DERIVATIVE) + self.assertTrue( + (e.kind == EffectKind.CONTINUOUS_INCREASE) + or (e.kind == EffectKind.CONTINUOUS_DECREASE) + ) if ele.name == "drag_ahead": found_drag_ahead = True self.assertTrue("engine_running" in str(ele)) From 6a40e8943f5e80c3bbfcd89a37121786e972c599 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 11:05:38 +0100 Subject: [PATCH 22/33] updated kinds and documentation, separated functions for increase/decrease --- unified_planning/model/problem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index e8b7f802e..7152a8d73 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -1035,7 +1035,6 @@ def update_problem_kind_action( self.update_action_timed_effect(t, e) continuous_fluents = set() fluents_in_rhs = set() - # TODO for t, le in action.effects.items(): for e in le: self.update_action_timed_effect(t, e) From cb4be062acd5662a3c782a791b827601b690a63c Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 11:28:54 +0100 Subject: [PATCH 23/33] moved checks for nonlinear effects on function for processes instead of actions --- unified_planning/model/natural_transition.py | 4 +-- unified_planning/model/problem.py | 32 ++++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index bd6120bf6..4305e6834 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -191,7 +191,7 @@ def add_continuous_increase( Adds the given `continuous increase effect` to the `process's effects`. NOTE description might be outdated - :param fluent: The `fluent` is the numeric state variable of which this process expresses its continuous increase, which in Newton's notation would be over-dot(fluent). + :param fluent: The `fluent` is the numeric state variable of which this process expresses its derivative, which in Newton's notation would be over-dot(fluent). :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. """ self._add_continuous_effect(fluent, value, False) @@ -205,7 +205,7 @@ def add_continuous_decrease( Adds the given `continuous decrease effect` to the `process's effects`. NOTE description might be outdated - :param fluent: The `fluent` is the numeric state variable of which this process expresses its continuous decrease, which in Newton's notation would be over-dot(fluent). + :param fluent: The `fluent` is the numeric state variable of which this process expresses its derivative, which in Newton's notation would be over-dot(fluent). :param value: This is the actual time derivative function. For instance, `fluent = 4` expresses that the time derivative of `fluent` is 4. """ self._add_continuous_effect(fluent, value, True) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 7152a8d73..934e14ca6 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -694,6 +694,8 @@ def _kind_factory(self) -> "_KindFactory": if len(self._timed_effects) > 0: factory.kind.set_time("CONTINUOUS_TIME") factory.kind.set_time("TIMED_EFFECTS") + for process in self._processes: + factory.update_problem_kind_process(process) for effect in chain(*self._timed_effects.values()): factory.update_problem_kind_effect(effect) if len(self._timed_goals) > 0: @@ -1033,18 +1035,6 @@ def update_problem_kind_action( for t, le in action.effects.items(): for e in le: self.update_action_timed_effect(t, e) - continuous_fluents = set() - fluents_in_rhs = set() - for t, le in action.effects.items(): - for e in le: - self.update_action_timed_effect(t, e) - continuous_fluents.add(e.fluent.fluent) - rhs = self.simplifier.simplify(e.value) - for var in self.environment.free_vars_extractor.get(rhs): - if var.is_fluent_exp(): - fluents_in_rhs.add(var.fluent) - if any(variable in fluents_in_rhs for variable in continuous_fluents): - self.kind.set_effects_kind("NON_LINEAR_CONTINUOUS_EFFECTS") if len(action.simulated_effects) > 0: self.kind.set_simulated_entities("SIMULATED_EFFECTS") @@ -1052,6 +1042,24 @@ def update_problem_kind_action( else: raise NotImplementedError + def update_problem_kind_process( + self, + process: "up.model.natural_transitions.Process", + ): + for param in process.parameters: + self.update_action_parameter(param) + + continuous_fluents = set() + fluents_in_rhs = set() + for e in process.effects: + continuous_fluents.add(e.fluent.fluent) + rhs = self.simplifier.simplify(e.value) + for var in self.environment.free_vars_extractor.get(rhs): + if var.is_fluent_exp(): + fluents_in_rhs.add(var.fluent) + if any(variable in fluents_in_rhs for variable in continuous_fluents): + self.kind.set_effects_kind("NON_LINEAR_CONTINUOUS_EFFECTS") + def update_problem_kind_metric( self, ) -> Tuple[Set["up.model.Fluent"], Set["up.model.Fluent"]]: From 788343e1d629e543c75f88f89c3c032f77f7a303 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 11:36:59 +0100 Subject: [PATCH 24/33] fix: typo --- unified_planning/model/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index 934e14ca6..eff08b858 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -1044,7 +1044,7 @@ def update_problem_kind_action( def update_problem_kind_process( self, - process: "up.model.natural_transitions.Process", + process: "up.model.natural_transition.Process", ): for param in process.parameters: self.update_action_parameter(param) From 41aa4c097b9ff038b6ce1026d843d4db015395c0 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 13:37:15 +0100 Subject: [PATCH 25/33] refactoring: changed kind names and related functions, fixed some bugs --- unified_planning/io/pddl_reader.py | 4 +-- unified_planning/io/pddl_writer.py | 4 ++- unified_planning/model/effect.py | 24 ++++++++------ unified_planning/model/natural_transition.py | 35 ++++++++------------ unified_planning/model/problem.py | 4 +-- unified_planning/test/examples/processes.py | 6 ++-- unified_planning/test/test_model.py | 4 +-- unified_planning/test/test_pddl_io.py | 4 +-- 8 files changed, 42 insertions(+), 43 deletions(-) diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index 5e75ede5b..70554cac4 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -655,7 +655,7 @@ def _add_effect( if isinstance(act, up.model.Process): eff1 = (eff[0], eff[1].simplify()) - act.add_continuous_increase(*eff1) + act.add_increase_continuous_effect(*eff1) else: act.add_increase_effect(*eff if timing is None else (timing, *eff)) # type: ignore elif op == "decrease": @@ -670,7 +670,7 @@ def _add_effect( ) if isinstance(act, up.model.Process): eff1 = (eff[0], eff[1].simplify()) - act.add_continuous_decrease(*eff1) + act.add_decrease_continuous_effect(*eff1) else: act.add_decrease_effect(*eff if timing is None else (timing, *eff)) # type: ignore elif op == "forall": diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index bc29d514f..2a9d4380a 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -1240,8 +1240,10 @@ def _write_derivative( out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") elif effect.is_decrease(): out.write(f" (decrease {fluent} (* #t {converter.convert(simplified_value)} ))") - elif effect.is_continuous_increase() or effect.is_continuous_decrease(): + elif effect.is_increase_continuous_effect(): out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") + elif effect.is_decrease_continuous_effect(): + out.write(f" (decrease {fluent} (* #t {converter.convert(simplified_value)} ))") else: raise UPProblemDefinitionError( "Derivative can only be expressed as increase, decrease in processes", diff --git a/unified_planning/model/effect.py b/unified_planning/model/effect.py index 236d50f72..ba9413858 100644 --- a/unified_planning/model/effect.py +++ b/unified_planning/model/effect.py @@ -38,15 +38,15 @@ class EffectKind(Enum): `ASSIGN` => `if C then F <= V` `INCREASE` => `if C then F <= F + V` `DECREASE` => `if C then F <= F - V` - `CONTINUOUS_INCREASE` => `dF/dt <= V` - `CONTINUOUS_DECREASE` => `dF/dt <= -V` + `INCREASE_CONTINUOUS_EFFECT` => `dF/dt <= V` + `DECREASE_CONTINUOUS_EFFECT` => `dF/dt <= -V` """ ASSIGN = auto() INCREASE = auto() DECREASE = auto() - CONTINUOUS_INCREASE = auto() - CONTINUOUS_DECREASE = auto() + INCREASE_CONTINUOUS_EFFECT = auto() + DECREASE_CONTINUOUS_EFFECT = auto() class Effect: @@ -113,7 +113,9 @@ def __repr__(self) -> str: s.append(f"forall {', '.join(str(v) for v in self._forall)}") if self.is_conditional(): s.append(f"if {str(self._condition)} then") - if not (self.is_continuous_increase() or self.is_continuous_decrease()): + if not ( + self.is_increase_continuous_effect() or self.is_decrease_continuous_effect() + ): s.append(f"{str(self._fluent)}") if self.is_assignment(): s.append(":=") @@ -121,8 +123,10 @@ def __repr__(self) -> str: s.append("+=") elif self.is_decrease(): s.append("-=") - elif self.is_continuous_increase() or self.is_continuous_decrease(): + elif self.is_increase_continuous_effect(): s.append(f"d{str(self._fluent)}/dt =") + elif self.is_decrease_continuous_effect(): + s.append(f"d{str(self._fluent)}/dt = -") s.append(f"{str(self._value)}") return " ".join(s) @@ -252,13 +256,13 @@ def is_decrease(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `decrease`, `False` otherwise.""" return self._kind == EffectKind.DECREASE - def is_continuous_increase(self) -> bool: + def is_increase_continuous_effect(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous increase`, `False` otherwise.""" - return self._kind == EffectKind.CONTINUOUS_INCREASE + return self._kind == EffectKind.INCREASE_CONTINUOUS_EFFECT - def is_continuous_decrease(self) -> bool: + def is_decrease_continuous_effect(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous decrease`, `False` otherwise.""" - return self._kind == EffectKind.CONTINUOUS_DECREASE + return self._kind == EffectKind.DECREASE_CONTINUOUS_EFFECT class SimulatedEffect: diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 4305e6834..768b69a8d 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -161,28 +161,21 @@ def _add_continuous_effect( ) if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): raise UPTypeError("Derivative can be created only on numeric types!") - if not negative: - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.CONTINUOUS_INCREASE, - forall=tuple(), - ) - ) - else: - self._add_effect_instance( - up.model.effect.Effect( - fluent_exp, - value_exp, - condition_exp, - kind=up.model.effect.EffectKind.CONTINUOUS_DECREASE, - forall=tuple(), - ) + e_kind = up.model.effect.EffectKind.INCREASE_CONTINUOUS_EFFECT + + if negative: + e_kind = up.model.effect.EffectKind.DECREASE_CONTINUOUS_EFFECT + self._add_effect_instance( + up.model.effect.Effect( + fluent_exp, + value_exp, + condition_exp, + kind=e_kind, + forall=tuple(), ) + ) - def add_continuous_increase( + def add_increase_continuous_effect( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], value: "up.model.expression.Expression", @@ -196,7 +189,7 @@ def add_continuous_increase( """ self._add_continuous_effect(fluent, value, False) - def add_continuous_decrease( + def add_decrease_continuous_effect( self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"], value: "up.model.expression.Expression", diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index eff08b858..b8fb24b40 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -888,9 +888,9 @@ def update_problem_kind_effect( self.kind.set_effects_kind("STATIC_FLUENTS_IN_OBJECT_ASSIGNMENTS") if any(f not in self.static_fluents for f in fluents_in_value): self.kind.set_effects_kind("FLUENTS_IN_OBJECT_ASSIGNMENTS") - elif e.is_continuous_increase(): + elif e.is_increase_continuous_effect(): self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") - elif e.is_continuous_decrease(): + elif e.is_decrease_continuous_effect(): self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") def update_problem_kind_expression( diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index cd0f4fd9e..36d69f36e 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -19,7 +19,7 @@ def get_example_problems(): b = Process("moving") b.add_precondition(on) - b.add_continuous_increase(d, 1) + b.add_increase_continuous_effect(d, 1) problem = Problem("1d_Movement") problem.add_fluent(on) @@ -54,11 +54,11 @@ def get_example_problems(): water_heating = Process("water_heating") water_heating.add_precondition(And(boiler_on, LE(temperature, 100))) - water_heating.add_continuous_increase(temperature, 1) + water_heating.add_increase_continuous_effect(temperature, 1) water_boiling = Process("water_boiling") water_boiling.add_precondition(And(boiler_on, GE(temperature, 100))) - water_boiling.add_continuous_decrease(water_level, 1) + water_boiling.add_decrease_continuous_effect(water_level, 1) open_chimney_vent_auto = Event("open_chimney_vent_auto") open_chimney_vent_auto.add_precondition( diff --git a/unified_planning/test/test_model.py b/unified_planning/test/test_model.py index 2c63e9f6e..b1fb8467f 100644 --- a/unified_planning/test/test_model.py +++ b/unified_planning/test/test_model.py @@ -126,12 +126,12 @@ def test_process(self): x = Fluent("x", IntType()) move = Process("moving", car=Vehicle) move.add_precondition(a) - move.add_continuous_increase(x, 1) + move.add_increase_continuous_effect(x, 1) e = Effect( FluentExp(x), Int(1), TRUE(), - unified_planning.model.EffectKind.CONTINUOUS_INCREASE, + unified_planning.model.EffectKind.INCREASE_CONTINUOUS_EFFECT, ) self.assertEqual(move.effects[0], e) self.assertEqual(move.name, "moving") diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 71fe65539..034b22617 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -451,8 +451,8 @@ def test_non_linear_car(self): if isinstance(ele, Process): for e in ele.effects: self.assertTrue( - (e.kind == EffectKind.CONTINUOUS_INCREASE) - or (e.kind == EffectKind.CONTINUOUS_DECREASE) + (e.kind == EffectKind.INCREASE_CONTINUOUS_EFFECT) + or (e.kind == EffectKind.DECREASE_CONTINUOUS_EFFECT) ) if ele.name == "drag_ahead": found_drag_ahead = True From 64fa4d8c887faf321df79aa397b0494519851159 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 13:49:40 +0100 Subject: [PATCH 26/33] updated contributors --- CONTRIBUTORS | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 455051a9e..1b33d67ba 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -23,3 +23,4 @@ Sebastian Stock Selvakumar Hastham Sathiya Satchi Sadanandam Srajan Goyal Uwe Köckemann +Samuel Gobbi From 2c49ac9c4558d7d205091bb31e72f29bc8d296cf Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Fri, 10 Jan 2025 14:15:24 +0100 Subject: [PATCH 27/33] refactoring: changed names --- unified_planning/io/pddl_writer.py | 4 ++-- unified_planning/model/effect.py | 24 +++++++++----------- unified_planning/model/natural_transition.py | 4 ++-- unified_planning/model/problem.py | 10 ++++++-- unified_planning/test/test_model.py | 2 +- unified_planning/test/test_pddl_io.py | 4 ++-- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 2a9d4380a..2059b96b6 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -1240,9 +1240,9 @@ def _write_derivative( out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") elif effect.is_decrease(): out.write(f" (decrease {fluent} (* #t {converter.convert(simplified_value)} ))") - elif effect.is_increase_continuous_effect(): + elif effect.is_continuous_increase(): out.write(f" (increase {fluent} (* #t {converter.convert(simplified_value)} ))") - elif effect.is_decrease_continuous_effect(): + elif effect.is_continuous_decrease(): out.write(f" (decrease {fluent} (* #t {converter.convert(simplified_value)} ))") else: raise UPProblemDefinitionError( diff --git a/unified_planning/model/effect.py b/unified_planning/model/effect.py index ba9413858..5996fd85b 100644 --- a/unified_planning/model/effect.py +++ b/unified_planning/model/effect.py @@ -38,15 +38,15 @@ class EffectKind(Enum): `ASSIGN` => `if C then F <= V` `INCREASE` => `if C then F <= F + V` `DECREASE` => `if C then F <= F - V` - `INCREASE_CONTINUOUS_EFFECT` => `dF/dt <= V` - `DECREASE_CONTINUOUS_EFFECT` => `dF/dt <= -V` + `CONTINUOUS_INCREASE` => `dF/dt <= V` + `CONTINUOUS_DECREASE` => `dF/dt <= -V` """ ASSIGN = auto() INCREASE = auto() DECREASE = auto() - INCREASE_CONTINUOUS_EFFECT = auto() - DECREASE_CONTINUOUS_EFFECT = auto() + CONTINUOUS_INCREASE = auto() + CONTINUOUS_DECREASE = auto() class Effect: @@ -113,9 +113,7 @@ def __repr__(self) -> str: s.append(f"forall {', '.join(str(v) for v in self._forall)}") if self.is_conditional(): s.append(f"if {str(self._condition)} then") - if not ( - self.is_increase_continuous_effect() or self.is_decrease_continuous_effect() - ): + if not (self.is_continuous_increase() or self.is_continuous_decrease()): s.append(f"{str(self._fluent)}") if self.is_assignment(): s.append(":=") @@ -123,9 +121,9 @@ def __repr__(self) -> str: s.append("+=") elif self.is_decrease(): s.append("-=") - elif self.is_increase_continuous_effect(): + elif self.is_continuous_increase(): s.append(f"d{str(self._fluent)}/dt =") - elif self.is_decrease_continuous_effect(): + elif self.is_continuous_decrease(): s.append(f"d{str(self._fluent)}/dt = -") s.append(f"{str(self._value)}") return " ".join(s) @@ -256,13 +254,13 @@ def is_decrease(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `decrease`, `False` otherwise.""" return self._kind == EffectKind.DECREASE - def is_increase_continuous_effect(self) -> bool: + def is_continuous_increase(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous increase`, `False` otherwise.""" - return self._kind == EffectKind.INCREASE_CONTINUOUS_EFFECT + return self._kind == EffectKind.CONTINUOUS_INCREASE - def is_decrease_continuous_effect(self) -> bool: + def is_continuous_decrease(self) -> bool: """Returns `True` if the :func:`kind ` of this `Effect` is a `continuous decrease`, `False` otherwise.""" - return self._kind == EffectKind.DECREASE_CONTINUOUS_EFFECT + return self._kind == EffectKind.CONTINUOUS_DECREASE class SimulatedEffect: diff --git a/unified_planning/model/natural_transition.py b/unified_planning/model/natural_transition.py index 768b69a8d..d783b785f 100644 --- a/unified_planning/model/natural_transition.py +++ b/unified_planning/model/natural_transition.py @@ -161,10 +161,10 @@ def _add_continuous_effect( ) if not fluent_exp.type.is_int_type() and not fluent_exp.type.is_real_type(): raise UPTypeError("Derivative can be created only on numeric types!") - e_kind = up.model.effect.EffectKind.INCREASE_CONTINUOUS_EFFECT + e_kind = up.model.effect.EffectKind.CONTINUOUS_INCREASE if negative: - e_kind = up.model.effect.EffectKind.DECREASE_CONTINUOUS_EFFECT + e_kind = up.model.effect.EffectKind.CONTINUOUS_DECREASE self._add_effect_instance( up.model.effect.Effect( fluent_exp, diff --git a/unified_planning/model/problem.py b/unified_planning/model/problem.py index b8fb24b40..fd6a3a2db 100644 --- a/unified_planning/model/problem.py +++ b/unified_planning/model/problem.py @@ -17,6 +17,7 @@ from itertools import chain, product import unified_planning as up +from unified_planning.model.effect import EffectKind import unified_planning.model.tamp from unified_planning.model import Fluent from unified_planning.model.abstract_problem import AbstractProblem @@ -888,9 +889,9 @@ def update_problem_kind_effect( self.kind.set_effects_kind("STATIC_FLUENTS_IN_OBJECT_ASSIGNMENTS") if any(f not in self.static_fluents for f in fluents_in_value): self.kind.set_effects_kind("FLUENTS_IN_OBJECT_ASSIGNMENTS") - elif e.is_increase_continuous_effect(): + elif e.is_continuous_increase(): self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") - elif e.is_decrease_continuous_effect(): + elif e.is_continuous_decrease(): self.kind.unset_problem_type("SIMPLE_NUMERIC_PLANNING") def update_problem_kind_expression( @@ -1052,6 +1053,11 @@ def update_problem_kind_process( continuous_fluents = set() fluents_in_rhs = set() for e in process.effects: + if e.kind == EffectKind.CONTINUOUS_INCREASE: + self.kind.set_effects_kind("INCREASE_CONTINUOUS_EFFECTS") + if e.kind == EffectKind.CONTINUOUS_DECREASE: + self.kind.set_effects_kind("DECREASE_CONTINUOUS_EFFECTS") + continuous_fluents.add(e.fluent.fluent) rhs = self.simplifier.simplify(e.value) for var in self.environment.free_vars_extractor.get(rhs): diff --git a/unified_planning/test/test_model.py b/unified_planning/test/test_model.py index b1fb8467f..dbb399668 100644 --- a/unified_planning/test/test_model.py +++ b/unified_planning/test/test_model.py @@ -131,7 +131,7 @@ def test_process(self): FluentExp(x), Int(1), TRUE(), - unified_planning.model.EffectKind.INCREASE_CONTINUOUS_EFFECT, + unified_planning.model.EffectKind.CONTINUOUS_INCREASE, ) self.assertEqual(move.effects[0], e) self.assertEqual(move.name, "moving") diff --git a/unified_planning/test/test_pddl_io.py b/unified_planning/test/test_pddl_io.py index 034b22617..71fe65539 100644 --- a/unified_planning/test/test_pddl_io.py +++ b/unified_planning/test/test_pddl_io.py @@ -451,8 +451,8 @@ def test_non_linear_car(self): if isinstance(ele, Process): for e in ele.effects: self.assertTrue( - (e.kind == EffectKind.INCREASE_CONTINUOUS_EFFECT) - or (e.kind == EffectKind.DECREASE_CONTINUOUS_EFFECT) + (e.kind == EffectKind.CONTINUOUS_INCREASE) + or (e.kind == EffectKind.CONTINUOUS_DECREASE) ) if ele.name == "drag_ahead": found_drag_ahead = True From 5fb2ab2689c468928cce3d111685568d93cd185d Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 14 Jan 2025 08:55:34 +0100 Subject: [PATCH 28/33] test: updated test_natural_transitions in test_problem --- unified_planning/test/test_problem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/unified_planning/test/test_problem.py b/unified_planning/test/test_problem.py index 1cce81f4c..14a2df448 100644 --- a/unified_planning/test/test_problem.py +++ b/unified_planning/test/test_problem.py @@ -609,16 +609,16 @@ def test_undefined_initial_state(self): def test_natural_transitions(self): p = self.problems["1d_movement"].problem - print(p) self.assertTrue(p.has_process("moving")) self.assertTrue(p.has_event("turn_off_automatically")) - proc = p.process("moving") - evt = p.event("turn_off_automatically") - print(proc) - print(evt) p.clear_events() p.clear_processes() self.assertEqual(len(p.natural_transitions), 0) + p_boiling_water = self.problems["boiling_water"].problem + print(p_boiling_water) + self.assertFalse(p_boiling_water.kind.has_non_linear_continuous_effects()) + self.assertTrue(p_boiling_water.kind.has_increase_continuous_effects()) + self.assertTrue(p_boiling_water.kind.has_decrease_continuous_effects()) if __name__ == "__main__": From 029bee40052f5c8986f60cde4c4b5a5cc5abb4ce Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 14 Jan 2025 08:55:51 +0100 Subject: [PATCH 29/33] test: updated test_natural_transitions in test_problem --- unified_planning/test/test_problem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/unified_planning/test/test_problem.py b/unified_planning/test/test_problem.py index 14a2df448..5958233bd 100644 --- a/unified_planning/test/test_problem.py +++ b/unified_planning/test/test_problem.py @@ -615,7 +615,6 @@ def test_natural_transitions(self): p.clear_processes() self.assertEqual(len(p.natural_transitions), 0) p_boiling_water = self.problems["boiling_water"].problem - print(p_boiling_water) self.assertFalse(p_boiling_water.kind.has_non_linear_continuous_effects()) self.assertTrue(p_boiling_water.kind.has_increase_continuous_effects()) self.assertTrue(p_boiling_water.kind.has_decrease_continuous_effects()) From ed3a2cb026f5a5a6e1302ca3f1238cd4f328d2b0 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 14 Jan 2025 11:52:21 +0100 Subject: [PATCH 30/33] chore: removed TODO comments --- unified_planning/io/pddl_writer.py | 1 - unified_planning/test/examples/processes.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 2059b96b6..0d0e9d58d 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -534,7 +534,6 @@ def _write_domain(self, out: IO[str]): costs: Dict[ Union[up.model.NaturalTransition, up.model.Action], Optional[up.model.FNode] ] = {} - # TODO check if natural_transition should be here metrics = self.problem.quality_metrics if len(metrics) == 1: metric = metrics[0] diff --git a/unified_planning/test/examples/processes.py b/unified_planning/test/examples/processes.py index 36d69f36e..74215144a 100644 --- a/unified_planning/test/examples/processes.py +++ b/unified_planning/test/examples/processes.py @@ -1,8 +1,6 @@ from unified_planning.shortcuts import * from unified_planning.test import TestCase -# TODO we need more tests for better coverage - def get_example_problems(): problems = {} From b1b18f1efb48450eb97ecf2ba0c0d01131fc6695 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Tue, 14 Jan 2025 12:09:43 +0100 Subject: [PATCH 31/33] test: added some coverage --- unified_planning/test/test_problem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unified_planning/test/test_problem.py b/unified_planning/test/test_problem.py index 5958233bd..c031853cf 100644 --- a/unified_planning/test/test_problem.py +++ b/unified_planning/test/test_problem.py @@ -609,8 +609,11 @@ def test_undefined_initial_state(self): def test_natural_transitions(self): p = self.problems["1d_movement"].problem + print(p) self.assertTrue(p.has_process("moving")) self.assertTrue(p.has_event("turn_off_automatically")) + print(p.process("moving")) + print(p.event("turn_off_automatically")) p.clear_events() p.clear_processes() self.assertEqual(len(p.natural_transitions), 0) From 1b9eae8e9b2118295d2ef57fbd6abb1d2db89698 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Wed, 15 Jan 2025 08:46:29 +0100 Subject: [PATCH 32/33] fix: added missing time requirement in pddl writer and reader --- unified_planning/io/pddl_reader.py | 2 +- unified_planning/io/pddl_writer.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/unified_planning/io/pddl_reader.py b/unified_planning/io/pddl_reader.py index 70554cac4..a20d6f99a 100644 --- a/unified_planning/io/pddl_reader.py +++ b/unified_planning/io/pddl_reader.py @@ -101,7 +101,7 @@ def __init__(self): + ":requirements" + OneOrMore( one_of( - ":strips :typing :negative-preconditions :disjunctive-preconditions :equality :existential-preconditions :universal-preconditions :quantified-preconditions :conditional-effects :fluents :numeric-fluents :adl :durative-actions :duration-inequalities :timed-initial-literals :timed-initial-effects :action-costs :hierarchy :method-preconditions :constraints :contingent :preferences" + ":strips :typing :negative-preconditions :disjunctive-preconditions :equality :existential-preconditions :universal-preconditions :quantified-preconditions :conditional-effects :fluents :numeric-fluents :adl :durative-actions :duration-inequalities :timed-initial-literals :timed-initial-effects :action-costs :hierarchy :method-preconditions :constraints :contingent :preferences :time" ) ) + Suppress(")") diff --git a/unified_planning/io/pddl_writer.py b/unified_planning/io/pddl_writer.py index 0d0e9d58d..c4f39f561 100644 --- a/unified_planning/io/pddl_writer.py +++ b/unified_planning/io/pddl_writer.py @@ -444,6 +444,8 @@ def _write_domain(self, out: IO[str]): out.write(" :hierarchy") # HTN / HDDL if self.problem_kind.has_method_preconditions(): out.write(" :method-preconditions") + if self.problem_kind.has_processes() or self.problem_kind.has_events(): + out.write(" :time") out.write(")\n") if self.problem_kind.has_hierarchical_typing(): From 19f1e1e484b8e771cd0aec2b492fa1c23627b927 Mon Sep 17 00:00:00 2001 From: Samuel Gobbi Date: Thu, 16 Jan 2025 08:47:55 +0100 Subject: [PATCH 33/33] chore: fixed contributors alphabetic order and added myself to contributors docs --- CONTRIBUTORS | 2 +- docs/contributions.rst | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 1b33d67ba..be6d7a39f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -19,8 +19,8 @@ Luca Framba Luigi Bonassi Miguel Ramirez Roland Godet +Samuel Gobbi Sebastian Stock Selvakumar Hastham Sathiya Satchi Sadanandam Srajan Goyal Uwe Köckemann -Samuel Gobbi diff --git a/docs/contributions.rst b/docs/contributions.rst index dad356e86..8b1056e8c 100644 --- a/docs/contributions.rst +++ b/docs/contributions.rst @@ -17,9 +17,10 @@ Contributors to the repository include: - Luca Framba - Luigi Bonassi - Roland Godet +- Samuel Gobbi - Selvakumar Hastham Sathiya Satchi Sadanandam - Srajan Goyal - Uwe Köckemann Alternate source for contributions: -https://github.com/aiplan4eu/unified-planning/graphs/contributors \ No newline at end of file +https://github.com/aiplan4eu/unified-planning/graphs/contributors