Modelling and Solving Planning Tasks
This tutorial proposes to explore the various features of the unified-planning library. The focus is deliberately kept on the most common planning problems (classical and numeric) to ease the presentation.
Let’s first install the unified planning library.
[ ]:
%pip install unified-planning[engines]
Classes to represent planning problems are in the
`unified_planning.model
<https://unified-planning.readthedocs.io/en/latest/api/model/index.html>`__ package.We also use the
`unified_planning.shortcuts
<https://github.com/aiplan4eu/unified-planning/blob/master/unified_planning/shortcuts.py>`__ package for simplified access to the most common features.
[1]:
from unified_planning.shortcuts import *
Modelling States
Fluents
Fluents define the language for specifying states
a
Fluent
is specified by aname
– a stringtype
– a fluent typesignature
– list of parameters (name + type with finite domain)
Available types relevant for classical and numerical problems
BoolType()
UserType(name, father=None)
IntType(lower_bound=None, upper_bound=None)
RealType(lower_bound=None, upper_bound=None)
[2]:
location = UserType("Location")
person = UserType("Person")
conference_attendee = UserType("Attendee", father=person)
laptop = UserType("Laptop")
[3]:
arrived = Fluent("arrived", BoolType(), person=conference_attendee)
destination = Fluent("destination", location, person=conference_attendee)
distance = Fluent("distance", IntType(0, 100), l_from=location, l_to=location)
battery_level = Fluent("battery_level", RealType(0.0, 1.0), l=laptop)
Objects
Declare an `Object
<https://unified-planning.readthedocs.io/en/latest/api/model/Object.html>`__ with Object(name, typename, environment=None)
.
[4]:
basel = Object("Basel", location)
prague = Object("Prague", location)
gabi = Object("Gabi", conference_attendee)
We can use objects to instantiate fluents:
[5]:
dest_gabi = destination(gabi)
arrived_gabi = arrived(gabi)
[6]:
print(arrived_gabi.__class__)
<class 'unified_planning.model.fnode.FNode'>
[7]:
arrived_gabi.fluent()
[7]:
bool arrived[person=Attendee - Person]
[8]:
arrived_gabi.args
[8]:
(Gabi,)
Creating a Problem
For classical, numeric and temporal planning, we use the class `Problem
<https://unified-planning.readthedocs.io/en/latest/api/model/Problem.html>`__.
[9]:
problem = Problem("Travel to ICAPS")
problem.add_fluent(destination, default_initial_value=prague)
problem.add_objects([gabi, basel, prague])
We don’t have to create fluents separately, but can also do so while adding them:
[10]:
fluent_at = problem.add_fluent("at", location, p=person)
[11]:
# You can also get hold of a fluent from its name:
problem.fluent("at")
[11]:
Location at[p=Person]
Analogously for objects:
[12]:
problem.add_object("Mum", person)
problem.add_objects(Object(name, conference_attendee) for name in ("Andrea", "Arthur", "Sebastian"))
problem.add_objects(Object(name, location) for name in ("Trento", "Toulouse", "Bremen"))
[13]:
list(problem.objects(conference_attendee))
[13]:
[Gabi, Andrea, Arthur, Sebastian]
[14]:
p = problem.object("Prague")
print(p, p.__class__)
Prague <class 'unified_planning.model.object.Object'>
How does the problem look like so far?
[15]:
print(problem)
problem name = Travel to ICAPS
types = [Location, Person, Attendee - Person]
fluents = [
Location destination[person=Attendee - Person]
Location at[p=Person]
]
actions = [
]
objects = [
Location: [Basel, Prague, Trento, Toulouse, Bremen]
Person: [Gabi, Mum, Andrea, Arthur, Sebastian]
Attendee - Person: [Gabi, Andrea, Arthur, Sebastian]
]
initial fluents default = [
Location destination[person=Attendee - Person] := Prague
]
initial values = [
]
goals = [
]
Let’s set the (rest of the) initial state:
[16]:
initially_at = [("Andrea", "Trento"), ("Arthur", "Toulouse"), ("Sebastian", "Bremen"),
("Gabi", "Basel"), ("Mum", "Basel")]
for name, loc in initially_at:
problem.set_initial_value(fluent_at(problem.object(name)), problem.object(loc))
[17]:
print(problem)
problem name = Travel to ICAPS
types = [Location, Person, Attendee - Person]
fluents = [
Location destination[person=Attendee - Person]
Location at[p=Person]
]
actions = [
]
objects = [
Location: [Basel, Prague, Trento, Toulouse, Bremen]
Person: [Gabi, Mum, Andrea, Arthur, Sebastian]
Attendee - Person: [Gabi, Andrea, Arthur, Sebastian]
]
initial fluents default = [
Location destination[person=Attendee - Person] := Prague
]
initial values = [
at(Andrea) := Trento
at(Arthur) := Toulouse
at(Sebastian) := Bremen
at(Gabi) := Basel
at(Mum) := Basel
]
goals = [
]
Goal
We need a goal. All attendees should be at their destination.
[18]:
a = Variable("a", conference_attendee)
problem.add_goal(Forall(fluent_at(a).Equals(destination(a)), a))
print(problem.goals)
[Forall (Attendee - Person a) (at(a) == destination(a))]
Actions
Let’s first add a very simple action to let persons travel between locations…
[19]:
travel = InstantaneousAction('travel', p=person, l_from=location, l_to=location)
p = travel.p
l_from = travel.l_from
l_to = travel.l_to
travel.add_precondition(fluent_at(p).Equals(l_from))
travel.add_effect(fluent_at(p), l_to)
problem.add_action(travel)
print(problem.actions)
[action travel(Person p, Location l_from, Location l_to) {
preconditions = [
(at(p) == l_from)
]
effects = [
at(p) := l_to
]
}]
Action Costs & Metrics
We also want to consider the travel times between the different locations and optimize the accumulated travel time of everyone getting to Prague.
To represent the action costs, we use an integer fluent and set random travel times (for the sake of simplicity).
[20]:
import itertools
from random import randint
travel_cost = Fluent("travel_cost", IntType(), from_loc=location, to_loc=location)
problem.add_fluent(travel_cost, default_initial_value=Int(0))
for loc1, loc2 in itertools.combinations(["Prague", "Trento", "Toulouse", "Bremen", "Basel"], 2):
dist = randint(1,100)
l1 = problem.object(loc1)
l2 = problem.object(loc2)
problem.set_initial_value(travel_cost(l1, l2), Int(dist))
problem.set_initial_value(travel_cost(l2, l1), Int(dist))
Now we can specify the metric as follows:
[21]:
m = MinimizeActionCosts({travel : travel_cost(travel.l_from, travel.l_to)}, default=Int(1))
problem.clear_quality_metrics()
problem.add_quality_metric(m)
Problem Kind
We wildly added everything without considering what fragment of planning we are targeting.
The library analyses what features we used by means of the
`ProblemKind
<https://unified-planning.readthedocs.io/en/latest/problem_representation.html#problem-kinds>`__.
[22]:
print(problem.kind)
PROBLEM_CLASS: ['ACTION_BASED']
CONDITIONS_KIND: ['UNIVERSAL_CONDITIONS', 'EQUALITIES']
TYPING: ['FLAT_TYPING', 'HIERARCHICAL_TYPING']
FLUENTS_TYPE: ['OBJECT_FLUENTS']
QUALITY_METRICS: ['ACTIONS_COST']
ACTIONS_COST_KIND: ['INT_NUMBERS_IN_ACTIONS_COST', 'STATIC_FLUENTS_IN_ACTIONS_COST']
It’s time to solve this incredibly hard problem…
but of course, object fluents are not widely supported by planning engines… And indeed:
[23]:
with OneshotPlanner(problem_kind=problem.kind) as planner:
res = planner.solve(problem)
print(res)
---------------------------------------------------------------------------
UPNoSuitableEngineAvailableException Traceback (most recent call last)
/tmp/ipykernel_173045/3821768881.py in <module>
----> 1 with OneshotPlanner(problem_kind=problem.kind) as planner:
2 res = planner.solve(problem)
3 print(res)
~/work/aries/planning/unified/deps/unified-planning/unified_planning/shortcuts.py in OneshotPlanner(name, names, params, problem_kind, optimality_guarantee)
548 | e.g. ``OneshotPlanner(problem_kind=problem.kind, optimality_guarantee=SOLVED_OPTIMALLY)``
549 """
--> 550 return get_environment().factory.OneshotPlanner(
551 name=name,
552 names=names,
~/work/aries/planning/unified/deps/unified-planning/unified_planning/engines/factory.py in OneshotPlanner(self, name, names, params, problem_kind, optimality_guarantee)
752 f"{optimality_guarantee} is not a valid OptimalityGuarantee."
753 )
--> 754 return self._get_engine(
755 OperationMode.ONESHOT_PLANNER,
756 name,
~/work/aries/planning/unified/deps/unified-planning/unified_planning/engines/factory.py in _get_engine(self, operation_mode, name, names, params, problem_kind, optimality_guarantee, compilation_kind, compilation_kinds, plan_kind, anytime_guarantee, problem)
657 params = {}
658 assert isinstance(params, Dict)
--> 659 EngineClass = self._get_engine_class(
660 operation_mode,
661 name,
~/work/aries/planning/unified/deps/unified-planning/unified_planning/engines/factory.py in _get_engine_class(self, operation_mode, name, problem_kind, optimality_guarantee, compilation_kind, plan_kind, anytime_guarantee)
544 else:
545 msg = f"No available {operation_mode} engine"
--> 546 raise up.exceptions.UPNoSuitableEngineAvailableException(msg)
547
548 def _print_credits(self, all_credits: Sequence[Optional["up.engines.Credits"]]):
UPNoSuitableEngineAvailableException: No available engine supports all the problem features:
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Engine | FLAT_TYPING | UNIVERSAL_CONDITIONS | INT_NUMBERS_IN_ACTIONS_COST | STATIC_FLUENTS_IN_ACTIONS_COST | OBJECT_FLUENTS | HIERARCHICAL_TYPING | ACTION_BASED | ACTIONS_COST | EQUALITIES |
=============================================================================================================================================================================================================================
| fast-downward | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| fast-downward-opt | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| symk | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| symk-opt | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| pyperplan | True | False | True | False | False | True | True | False | False |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| pyperplan-opt | True | False | True | False | False | True | True | False | False |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| enhsp | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| enhsp-opt | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| enhsp-any | True | True | True | True | False | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| tamer | True | False | True | False | True | False | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| lpg | True | False | True | True | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| lpg-anytime | True | False | True | True | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| lpg-repairer | True | False | True | True | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| fmap | True | True | True | False | True | True | False | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| aries | True | False | True | True | True | True | True | True | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[fast-downward] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[fast-downward-opt] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[symk] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[symk-opt] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[enhsp] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[enhsp-opt] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[enhsp-any] | True | True | True | False | False | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[tamer] | True | False | True | False | True | False | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| oversubscription[aries] | True | False | False | False | True | True | True | False | True |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Using a Compiler
For Fast Downward, the only unsupported feature were the object fluents.
Let’s use a compiler to eliminate them:
[24]:
with Compiler(
problem_kind = problem.kind,
compilation_kind = CompilationKind.USERTYPE_FLUENTS_REMOVING
) as usertype_remover:
utr_result = usertype_remover.compile(
problem,
CompilationKind.USERTYPE_FLUENTS_REMOVING
)
[25]:
utr_problem = utr_result.problem
print(utr_problem)
problem name = utfr_Travel to ICAPS
types = [Person, Attendee - Person, Location]
fluents = [
bool destination[person=Attendee - Person, location=Location]
bool at[p=Person, location=Location]
integer travel_cost[from_loc=Location, to_loc=Location]
]
actions = [
action travel(Person p, Location l_from, Location l_to) {
preconditions = [
at(p, l_from)
]
effects = [
if (l_to == Basel) then at(p, Basel) := true
if (not (l_to == Basel)) then at(p, Basel) := false
if (l_to == Prague) then at(p, Prague) := true
if (not (l_to == Prague)) then at(p, Prague) := false
if (l_to == Trento) then at(p, Trento) := true
if (not (l_to == Trento)) then at(p, Trento) := false
if (l_to == Toulouse) then at(p, Toulouse) := true
if (not (l_to == Toulouse)) then at(p, Toulouse) := false
if (l_to == Bremen) then at(p, Bremen) := true
if (not (l_to == Bremen)) then at(p, Bremen) := false
]
}
]
objects = [
Person: [Gabi, Mum, Andrea, Arthur, Sebastian]
Attendee - Person: [Gabi, Andrea, Arthur, Sebastian]
Location: [Basel, Prague, Trento, Toulouse, Bremen]
]
initial fluents default = [
]
initial values = [
at(Andrea, Basel) := false
at(Andrea, Prague) := false
at(Andrea, Trento) := true
at(Andrea, Toulouse) := false
at(Andrea, Bremen) := false
at(Arthur, Basel) := false
at(Arthur, Prague) := false
at(Arthur, Trento) := false
at(Arthur, Toulouse) := true
at(Arthur, Bremen) := false
at(Sebastian, Basel) := false
at(Sebastian, Prague) := false
at(Sebastian, Trento) := false
at(Sebastian, Toulouse) := false
at(Sebastian, Bremen) := true
at(Gabi, Basel) := true
at(Gabi, Prague) := false
at(Gabi, Trento) := false
at(Gabi, Toulouse) := false
at(Gabi, Bremen) := false
at(Mum, Basel) := true
at(Mum, Prague) := false
at(Mum, Trento) := false
at(Mum, Toulouse) := false
at(Mum, Bremen) := false
travel_cost(Prague, Trento) := 46
travel_cost(Trento, Prague) := 46
travel_cost(Prague, Toulouse) := 76
travel_cost(Toulouse, Prague) := 76
travel_cost(Prague, Bremen) := 81
travel_cost(Bremen, Prague) := 81
travel_cost(Prague, Basel) := 24
travel_cost(Basel, Prague) := 24
travel_cost(Trento, Toulouse) := 67
travel_cost(Toulouse, Trento) := 67
travel_cost(Trento, Bremen) := 12
travel_cost(Bremen, Trento) := 12
travel_cost(Trento, Basel) := 32
travel_cost(Basel, Trento) := 32
travel_cost(Toulouse, Bremen) := 59
travel_cost(Bremen, Toulouse) := 59
travel_cost(Toulouse, Basel) := 10
travel_cost(Basel, Toulouse) := 10
travel_cost(Bremen, Basel) := 94
travel_cost(Basel, Bremen) := 94
destination(Gabi, Basel) := false
destination(Gabi, Prague) := true
destination(Gabi, Trento) := false
destination(Gabi, Toulouse) := false
destination(Gabi, Bremen) := false
destination(Andrea, Basel) := false
destination(Andrea, Prague) := true
destination(Andrea, Trento) := false
destination(Andrea, Toulouse) := false
destination(Andrea, Bremen) := false
destination(Arthur, Basel) := false
destination(Arthur, Prague) := true
destination(Arthur, Trento) := false
destination(Arthur, Toulouse) := false
destination(Arthur, Bremen) := false
destination(Sebastian, Basel) := false
destination(Sebastian, Prague) := true
destination(Sebastian, Trento) := false
destination(Sebastian, Toulouse) := false
destination(Sebastian, Bremen) := false
travel_cost(Basel, Basel) := 0
travel_cost(Prague, Prague) := 0
travel_cost(Trento, Trento) := 0
travel_cost(Toulouse, Toulouse) := 0
travel_cost(Bremen, Bremen) := 0
]
goals = [
Forall (Attendee - Person a) Exists (Location destination_location) (at(a, destination_location) and destination(a, destination_location))
]
quality metrics = [
minimize actions-cost: {'travel': travel_cost(l_from, l_to), 'default': None}
]
[26]:
utr_map_back = utr_result.map_back_action_instance
print(utr_map_back)
functools.partial(<function replace_action at 0x7f7c5f3965f0>, map={action travel(Person p, Location l_from, Location l_to) {
preconditions = [
at(p, l_from)
]
effects = [
if (l_to == Basel) then at(p, Basel) := true
if (not (l_to == Basel)) then at(p, Basel) := false
if (l_to == Prague) then at(p, Prague) := true
if (not (l_to == Prague)) then at(p, Prague) := false
if (l_to == Trento) then at(p, Trento) := true
if (not (l_to == Trento)) then at(p, Trento) := false
if (l_to == Toulouse) then at(p, Toulouse) := true
if (not (l_to == Toulouse)) then at(p, Toulouse) := false
if (l_to == Bremen) then at(p, Bremen) := true
if (not (l_to == Bremen)) then at(p, Bremen) := false
]
}: action travel(Person p, Location l_from, Location l_to) {
preconditions = [
(at(p) == l_from)
]
effects = [
at(p) := l_to
]
}})
Let’s use this to find a plan for the original problem.
[27]:
with OneshotPlanner(problem_kind=utr_problem.kind) as planner:
res = planner.solve(utr_problem)
NOTE: To disable printing of planning engine credits, add this line to your code: `up.shortcuts.get_environment().credits_stream = None`
*** Credits ***
* In operation mode `OneshotPlanner` at line 1 of `/tmp/ipykernel_173045/1887964203.py`, you are using the following planning engine:
* Engine name: Fast Downward
* Developers: Uni Basel team and contributors (cf. https://github.com/aibasel/downward/blob/main/README.md)
* Description: Fast Downward is a domain-independent classical planning system.
[28]:
print(res.status)
PlanGenerationResultStatus.SOLVED_SATISFICING
[30]:
utr_plan = res.plan
print(utr_plan)
SequentialPlan:
travel(Sebastian, Bremen, Prague)
travel(Gabi, Basel, Prague)
travel(Arthur, Toulouse, Prague)
travel(Andrea, Trento, Prague)
This is a plan for the compiled task. To transform it into a task for the original problem, we need to map the actions back (not immediately visible because the actions have the same names):
[31]:
plan = utr_plan.replace_action_instances(utr_map_back)
plan
[31]:
SequentialPlan([travel(Sebastian, Bremen, Prague), travel(Gabi, Basel, Prague), travel(Arthur, Toulouse, Prague), travel(Andrea, Trento, Prague)])
[32]:
act = plan.actions[-1] # TRY: compare last action of utr_plan and plan
act.action
[32]:
action travel(Person p, Location l_from, Location l_to) {
preconditions = [
(at(p) == l_from)
]
effects = [
at(p) := l_to
]
}
Currently supported compilation kinds:
GROUNDING
CONDITIONAL_EFFECTS_REMOVING
DISJUNCTIVE_CONDITIONS_REMOVING
NEGATIVE_CONDITIONS_REMOVING
QUANTIFIERS_REMOVING
TRAJECTORY_CONSTRAINTS_REMOVING
USERTYPE_FLUENTS_REMOVING
BOUNDED_TYPES_REMOVING
STATE_INVARIANTS_REMOVING
Solving a task optimally
Remember the result status of our PlanGenerationResult?
[33]:
print(res.status)
PlanGenerationResultStatus.SOLVED_SATISFICING
We would like to find an optimal plan (with guarantee!). This can be requested with:
[34]:
from unified_planning.engines import PlanGenerationResultStatus
with OneshotPlanner(problem_kind=utr_problem.kind,
optimality_guarantee=PlanGenerationResultStatus.SOLVED_OPTIMALLY) as planner:
res = planner.solve(utr_problem)
*** Credits ***
* In operation mode `OneshotPlanner` at line 3 of `/tmp/ipykernel_173045/1418646784.py`, you are using the following planning engine:
* Engine name: SymK
* Developers: David Speck (cf. https://github.com/speckdavid/symk/blob/master/README.md )
* Description: SymK is a state-of-the-art domain-independent classical optimal and top-k planner.
[35]:
print(res.plan)
SequentialPlan:
travel(Sebastian, Bremen, Trento)
travel(Sebastian, Trento, Prague)
travel(Andrea, Trento, Prague)
travel(Gabi, Basel, Prague)
travel(Arthur, Toulouse, Basel)
travel(Arthur, Basel, Prague)
More on Actions
InstantaneousAction
s can have arbitrary formulas as preconditions as well as universal and conditional effects. Let’s consider another action that exploits these features.
Formulas are most easily build using shortcuts. Here we will use Equals
.
[36]:
can_beam = Fluent("can_beam", BoolType(), p=person)
beam = InstantaneousAction('beam_persons_to_prague')
# reminder: a was a variable of type person
beam.add_precondition(Exists(Not(Equals(fluent_at(a), prague)), a))
beam.add_effect(fluent_at(a), prague, condition=can_beam(a), forall=[a])
print(beam)
action beam_persons_to_prague {
preconditions = [
Exists (Attendee - Person a) (not (at(a) == Prague))
]
effects = [
forall Attendee - Person a if can_beam(a) then at(a) := Prague
]
}
Getting Help
We can use python’s built-in help functionality to get help on any object.
[37]:
help(beam.add_effect) # also try beam.add_effect
Help on method add_effect in module unified_planning.model.action:
add_effect(fluent: Union[ForwardRef('up.model.fnode.FNode'), ForwardRef('up.model.fluent.Fluent')], value: 'up.model.expression.Expression', condition: 'up.model.expression.BoolExpression' = True, forall: Iterable[ForwardRef('up.model.variable.Variable')] = ()) method of unified_planning.model.action.InstantaneousAction instance
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.
Numeric Planning
No formal distinction of classical and numeric planning
It’s all in the problem kind: if you use numeric features, you will need a numeric planner that supports them.
[38]:
acceptable_changes = Fluent("acceptable_changes", IntType(), p=person)
total_travel_time = Fluent("travel_time", IntType())
problem.add_fluent(acceptable_changes, default_initial_value=2)
problem.add_fluent(total_travel_time, default_initial_value=0)
[38]:
integer travel_time
Extension of formulas (for preconditions and goals) * Numeric expressions can be combined by operands +, -, , / Numeric expressions can be compared with \(\le\) (LE
), \(\ge\) (GE
), \(<\) (LT
), \(>\) (GT
), \(=\) (Equals
)
[39]:
travel.add_precondition(acceptable_changes(p) >= 1)
print(travel)
action travel(Person p, Location l_from, Location l_to) {
preconditions = [
(at(p) == l_from)
(1 <= acceptable_changes(p))
]
effects = [
at(p) := l_to
]
}
Besides the normal (assignment) effects, we can also have increase and decrease effects:
[40]:
travel.add_decrease_effect(acceptable_changes(travel.p), 1)
travel.add_increase_effect(total_travel_time(), travel_cost(travel.l_from, travel.l_to))
print(travel)
action travel(Person p, Location l_from, Location l_to) {
preconditions = [
(at(p) == l_from)
(1 <= acceptable_changes(p))
]
effects = [
at(p) := l_to
acceptable_changes(p) -= 1
travel_time += travel_cost(l_from, l_to)
]
}
Let’s also use a new optimization metric…
[41]:
problem.clear_quality_metrics()
m = MaximizeExpressionOnFinalState(acceptable_changes(problem.object("Andrea")) +
acceptable_changes(problem.object("Sebastian")) +
acceptable_changes(problem.object("Arthur")) +
acceptable_changes(problem.object("Gabi")) -
total_travel_time()/100)
problem.add_quality_metric(m)
m
[41]:
maximize ((((acceptable_changes(Andrea) + acceptable_changes(Sebastian)) + acceptable_changes(Arthur)) + acceptable_changes(Gabi)) - (travel_time / 100))
[42]:
with Compiler(
problem_kind = problem.kind,
compilation_kind = CompilationKind.USERTYPE_FLUENTS_REMOVING
) as usertype_remover:
utr_result = usertype_remover.compile(
problem,
CompilationKind.USERTYPE_FLUENTS_REMOVING
)
utr_problem = utr_result.problem
with OneshotPlanner(problem_kind=utr_problem.kind) as planner:
res = planner.solve(utr_problem)
print(res)
*** Credits ***
* In operation mode `OneshotPlanner` at line 10 of `/tmp/ipykernel_173045/277381419.py`, you are using the following planning engine:
* Engine name: ENHSP
* Developers: Enrico Scala
* Description: Expressive Numeric Heuristic Search Planner.
status: SOLVED_SATISFICING
engine: enhsp
plan: SequentialPlan:
travel(Sebastian, Bremen, Prague)
travel(Arthur, Toulouse, Prague)
travel(Andrea, Trento, Prague)
travel(Gabi, Basel, Prague)