Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Support for Axioms and Derived Predicates #493

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lpg": ["up-lpg==0.0.7"],
"fmap": ["up-fmap==0.0.7"],
"aries": ["up-aries>=0.0.8"],
"symk": ["up-symk>=0.0.3"],
"symk": ["up-symk>=1.0.0"],
"engines": [
"tarski[arithmetic]",
"up-pyperplan==1.0.0.1.dev1",
Expand All @@ -40,7 +40,7 @@
"up-lpg==0.0.7",
"up-fmap==0.0.7",
"up-aries>=0.0.8",
"up-symk>=0.0.3",
"up-symk>=1.0.0",
],
"plot": [
"plotly",
Expand Down
75 changes: 71 additions & 4 deletions unified_planning/io/pddl_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 :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 :action-costs :hierarchy :method-preconditions :constraints :contingent :preferences :derived-predicates"
)
)
+ Suppress(")")
Expand Down Expand Up @@ -168,6 +168,14 @@ def __init__(self):
+ Suppress(")")
)

axiom_def = Group(
Suppress("(")
+ ":derived"
- set_results_name(predicate, "head")
- set_results_name(nested_expr(), "body")
+ Suppress(")")
)

parameters = set_results_name(
ZeroOrMore(
Group(
Expand All @@ -176,6 +184,7 @@ def __init__(self):
),
"params",
)

action_def = Group(
Suppress("(")
+ ":action"
Expand Down Expand Up @@ -255,6 +264,7 @@ def __init__(self):
+ Optional(constants_def)
+ Optional(predicates_def)
+ Optional(functions_def)
+ set_results_name(Group(ZeroOrMore(axiom_def)), "axioms")
+ set_results_name(Group(ZeroOrMore(task_def)), "tasks")
+ set_results_name(Group(ZeroOrMore(method_def)), "methods")
+ set_results_name(
Expand Down Expand Up @@ -387,7 +397,9 @@ 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.Action, up.model.Axiom, htn.Method, htn.TaskNetwork]
],
types_map: TypesMap,
var: Dict[str, up.model.Variable],
exp: CustomParseResults,
Expand Down Expand Up @@ -1042,8 +1054,7 @@ def declare_type(

has_actions_cost = False

for p in domain_res.get("predicates", []):
n = p[0]
def get_fluent_params(p: ParseResults) -> OrderedDict():
params = OrderedDict()
for g in p[1]:
try:
Expand All @@ -1061,6 +1072,11 @@ def declare_type(
)
for param_name in g.value[0]:
params[param_name] = param_type
return params

for p in domain_res.get("predicates", []):
n = p[0]
params = get_fluent_params(p)
f = up.model.Fluent(n, self._tm.BoolType(), params, self._env)
problem.add_fluent(f)

Expand Down Expand Up @@ -1142,6 +1158,57 @@ def declare_type(
task = htn.Task(name, task_params)
problem.add_task(task)

for axiom_entry in domain_res.get("axioms", []):
# Each axiom should have only one predicate in the head
assert (
len(axiom_entry["head"]) == 1
), "Only one predicate in head of axiom allowed"

# Extract the fluent name from the axiom's head
fluent_name = axiom_entry["head"][0][0]

# Set the fluent's type to DerivedBoolType
problem.fluent(fluent_name)._typename = self._tm.DerivedBoolType()

# Extract and organize the axiom's parameters
axiom_params = OrderedDict()
for param_entry in axiom_entry["head"][0][1]:
param_name, param_type = param_entry[1][0][0], param_entry[1][1]
axiom_params[param_name] = types_map[
param_type if len(param_entry[1]) > 1 else Object
]

# Create an Axiom object with the parameters
axiom = unified_planning.model.Axiom("", axiom_params)

# Retrieve the current fluent
this_fluent = problem.fluent(fluent_name)

# Create an effect using the axiom's parameters and set it as the axiom's head
effect = this_fluent(*axiom.parameters)
axiom.set_head(effect)

# Ensure there's only one condition in the body of the axiom
assert (
len(axiom_entry["body"]) == 1
), "Only one condition in body of axiom allowed"

# Parse the condition and add it to the axiom's body
body_condition = self._parse_exp(
problem,
axiom,
types_map,
{},
CustomParseResults(axiom_entry["body"][0]),
domain_str,
)

# Add the parsed body condition to the axiom
axiom.add_body_condition(body_condition)

# Add the axiom to the problem
problem.add_axiom(axiom)

for a in domain_res.get("actions", []):
n = a["name"]
a_params = OrderedDict()
Expand Down
27 changes: 26 additions & 1 deletion unified_planning/io/pddl_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ def _write_domain(self, out: IO[str]):
or self.problem_kind.has_fluents_in_actions_cost()
):
out.write(" :numeric-fluents")
if self.problem_kind.has_derived_fluents():
out.write(" :derived-predicates")
if self.problem_kind.has_conditional_effects():
out.write(" :conditional-effects")
if self.problem_kind.has_existential_conditions():
Expand Down Expand Up @@ -477,7 +479,7 @@ def _write_domain(self, out: IO[str]):
predicates = []
functions = []
for f in self.problem.fluents:
if f.type.is_bool_type():
if f.type.is_bool_type() or f.type.is_derived_bool_type():
params = []
i = 0
for param in f.signature:
Expand Down Expand Up @@ -533,6 +535,29 @@ def _write_domain(self, out: IO[str]):
"Only one metric is supported!"
)

for a in self.problem.axioms:
if any(p.simplify().is_false() for p in a.preconditions):
continue
out.write(f" (:derived ({self._get_mangled_name(a.head._fluent.fluent())}")
for ap in a.parameters:
if ap.type.is_user_type():
out.write(
f" {self._get_mangled_name(ap)} - {self._get_mangled_name(ap.type)}"
)
else:
raise UPTypeError("PDDL supports only user type parameters")
out.write(")\n")
if len(a.preconditions) > 0:
precond_str: List[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' (and {" ".join(precond_str)})\n')
out.write(" )\n")

em = self.problem.environment.expression_manager
for a in self.problem.actions:
if isinstance(a, up.model.InstantaneousAction):
Expand Down
2 changes: 2 additions & 0 deletions unified_planning/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
DurativeAction,
SensingAction,
)
from unified_planning.model.axiom import Axiom
from unified_planning.model.effect import Effect, SimulatedEffect, EffectKind
from unified_planning.model.expression import (
BoolExpression,
Expand Down Expand Up @@ -133,4 +134,5 @@
"Oversubscription",
"TemporalOversubscription",
"DeltaSimpleTemporalNetwork",
"Axiom",
]
14 changes: 12 additions & 2 deletions unified_planning/model/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,12 @@ def add_precondition(
(precondition_exp,) = self._environment.expression_manager.auto_promote(
precondition
)
assert self._environment.type_checker.get_type(precondition_exp).is_bool_type()
assert (
self._environment.type_checker.get_type(precondition_exp).is_bool_type()
or self._environment.type_checker.get_type(
precondition_exp
).is_derived_bool_type()
)
if precondition_exp == self._environment.expression_manager.TRUE():
return
free_vars = self._environment.free_vars_oracle.get_free_variables(
Expand Down Expand Up @@ -341,7 +346,12 @@ def add_effect(
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():
if not (
self._environment.type_checker.get_type(condition_exp).is_bool_type()
or self._environment.type_checker.get_type(
condition_exp
).is_derived_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).
Expand Down
112 changes: 112 additions & 0 deletions unified_planning/model/axiom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 base class `Axiom'.
An `Axiom' has a `head' which is a single `Parameter' and a `body' which is a single `Condition'.
"""

from unified_planning.model.action import *
from unified_planning.environment import get_environment, Environment


class Axiom(InstantaneousAction):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deriving from InstantaneousAction? This makes it possible to have Axioms passed to the add_action methods and causes some hacky code below. Why not a dedicated class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was mainly to reuse the functionality and code of actions, not only when defining an action, but also for PDDL parsing. But I agree that we may have some side effects here. In general, I am open to creating a dedicated class not derived from InstantaneousAction, but I am a bit concerned that the PDDL parsing might become a bit more involved and we might get some redundant code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have a look at the internal structure of the InstantaneousActiona and see what is that you really need? Maybe this could be a call to a small refactoring aimed at factorising what is needed to both instantaneous actions and axioms.

def __init__(
self,
_name: str,
_parameters: Optional["OrderedDict[str, up.model.types.Type]"] = None,
_env: Optional[Environment] = None,
**kwargs: "up.model.types.Type",
):
InstantaneousAction.__init__(self, _name, _parameters, _env, **kwargs)

def set_head(self, fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"]):
if not fluent.type.is_derived_bool_type():
raise UPTypeError("The head of an axiom must be of type DerivedBoolType!")

self.add_effect(fluent)

def add_body_condition(
self,
precondition: Union[
"up.model.fnode.FNode",
"up.model.fluent.Fluent",
"up.model.parameter.Parameter",
bool,
],
):
super().add_precondition(precondition)

def add_effect(
self,
fluent: Union["up.model.fnode.FNode", "up.model.fluent.Fluent"],
value: "up.model.expression.Expression" = True,
condition: "up.model.expression.BoolExpression" = True,
forall: Iterable["up.model.variable.Variable"] = tuple(),
):
if value != True:
raise UPUsageError("value can only be true for an axiom")
if condition != True:
raise UPUsageError("the effect of an axiom can not include a condition")
if len(forall) > 0:
raise UPUsageError("the effect of an axiom can not a forall")

(
fluent_exp,
value_exp,
condition_exp,
) = self._environment.expression_manager.auto_promote(fluent, value, condition)

if not fluent_exp.is_fluent_exp():
raise UPUsageError("head/effect of an axiom must be a fluent expression")

fluent_exp_params = [p.parameter() for p in fluent_exp.args]
if self.parameters != fluent_exp_params:
raise UPUsageError(
f"parameters of axiom and this fluent expression do not match: {self.parameters} vs. {fluent_exp_params}"
)

self.clear_effects()
self._add_effect_instance(
up.model.effect.Effect(fluent_exp, value_exp, condition_exp, forall=forall)
)

@property
def head(self) -> List["up.model.fluent.Fluent"]:
"""Returns the `head` of the `Axiom`."""
return self._effects[0]

@property
def body(self) -> List["up.model.fnode.FNode"]:
"""Returns the `list` of the `Axiom` `body`."""
return self._preconditions

def __repr__(self) -> str:
s = []
s.append(f"axiom {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(f" head = {self.head}\n")
s.append(f" body = {self.body}\n")
s.append(" }")
return "".join(s)
2 changes: 2 additions & 0 deletions unified_planning/model/mixins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#

from unified_planning.model.mixins.actions_set import ActionsSetMixin
from unified_planning.model.mixins.axioms_set import AxiomsSetMixin
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
Expand All @@ -24,6 +25,7 @@

__all__ = [
"ActionsSetMixin",
"AxiomsSetMixin",
"TimeModelMixin",
"FluentsSetMixin",
"ObjectsSetMixin",
Expand Down
Loading