Problem Representation

The main functionality offered by the library concerns the specification of a planning problem. In particular, the UP library supports the following classes of planning problems: Classical, Numeric, Temporal, Scheduling, Multi-Agent, Hierarchical, Task and Motion Planning (TAMP) and Conformant. For each of these, we provide a simple description and a reference code at the end of the section.

One of the key element of the problem specifications is the ProblemKind class (automatically computed by all the planning problems classes via the kind property), which is a collection of flags, documented in the table below, that identifies the modeling features used in any problem specification, so that the library can determine the applicability of each engine for a certain query.

Problem Kinds

Features

ProblemKind flag

Description

PROBLEM_CLASS

ACTION_BASED

The problem is an action-based classical, numeric or temporal planning problem.

HIERARCHICAL

The problem is an action-based problem with hierarchical features.

CONTINGENT

The problem is an action-based problem with non-deterministic initial state and observation (called sensing) actions.

ACTION_BASED_MULTI_AGENT

The problem is an action-based problem where action and fluents can be divided in more than one agent.

SCHEDULING

The problem is a scheduling problem where a known set of activities need to be scheduled in time.

TAMP

The problem is a Task and Motion Planning problem.

PROBLEM_TYPE

SIMPLE_NUMERIC_PLANNING

The numeric part of the problem exhibits only linear numeric conditions of the form f(X) {>=,>,=} 0 where f(X) is a linear expression constructed over numeric variables X. Moreover, effects are restricted to increase and decrease operations by a constant. For instance x+=k with k constant is allowed, while x+=y with y variable is not.

GENERAL_NUMERIC_PLANNING

The problem uses numeric planning using unrestricted arithmetic.

TIME

CONTINUOUS_TIME

The temporal planning problem is defined over a continuous time model.

DISCRETE_TIME

The temporal planning problem is defined over a discrete time model.

INTERMEDIATE_CONDITIONS_AND_EFFECTS

The temporal planning problem uses either conditions or effects happening during an action (not just at the beginning or end of the interval).

EXTERNAL_CONDITIONS_AND_EFFECTS

The temporal planning problem uses either conditions or effects happening outside the interval of an action (e.g. 10 seconds after the end of the action).

TIMED_EFFECTS

The temporal planning problem has effects scheduled at absolute times (e.g., Timed Initial Literals in PDDL).

TIMED_GOALS

The temporal planning problem uses goals required to be true at times different from the end of the plan.

DURATION_INEQUALITIES

The temporal planning problem has at least one action with non-constant duration (and instead uses a lower bound different than upper bound).

SELF_OVERLAPPING

The temporal planning problem allows actions self overlapping.

EXPRESSION_DURATION

STATIC_FLUENTS_IN_DURATION

The duration of at least one action uses static fluents (that may never change).

FLUENTS_IN_DURATION

The duration of at least one action is specified using non-static fluents (that might change over the course of a plan).

NUMBERS

CONTINUOUS_NUMBERS

The problem uses numbers ranging over continuous domains (e.g. reals).

DISCRETE_NUMBERS

The problem uses numbers ranging over discrete domains (e.g. integers).

BOUNDED_TYPES

The problem uses bounded-domain numbers.

CONDITIONS_KIND

NEGATIVE_CONDITIONS

The problem has at least one condition using the negation Boolean operator.

DISJUNCTIVE_CONDITIONS

The problem has at least one condition using the Boolean “or” operator.

EQUALITIES

The problem has at least one condition using the equality predicate (usually over two finite-domain variables or object fluents).

EXISTENTIAL_CONDITIONS

The problem has at least a condition using the “exists” quantifier over problem objects.

UNIVERSAL_CONDITIONS

The problem has at least a condition using the “forall” quantifier over problem objects.

EFFECTS_KIND

CONDITIONAL_EFFECTS

At least one effect has a condition.

FORALL_EFFECTS

At least one effect uses the “forall” quantifier over problem objects.

INCREASE_EFFECTS

At least one effect uses the numeric increment operator.

DECREASE_EFFECTS

At least one effect uses the numeric decrement operator.

STATIC_FLUENTS_IN_BOOLEAN_ASSIGNMENTS

At least one effect uses a static fluent in the expression of a boolean assignment.

STATIC_FLUENTS_IN_NUMERIC_ASSIGNMENTS

At least one effect uses a static fluent in the expression of a numeric assignment.

STATIC_FLUENTS_IN_OBJECT_ASSIGNMENTS

At least one effect uses a static fluent in the expression of a object assignment.

FLUENTS_IN_BOOLEAN_ASSIGNMENTS

At least one effect uses a fluent in the expression of a boolean assignment.

FLUENTS_IN_NUMERIC_ASSIGNMENTS

At least one effect uses a fluent in the expression of a numeric assignment.

FLUENTS_IN_OBJECT_ASSIGNMENTS

At least one effect uses a fluent in the expression of a object assignment.

TYPING

FLAT_TYPING

The problem uses user-defined types, but no type inherits from another.

HIERARCHICAL_TYPING

At least one user-defined type in the problem inherits from another type.

PARAMETERS

BOOL_FLUENT_PARAMETERS

At least one fluent has a parameter of boolean type.

BOUNDED_INT_FLUENT_PARAMETERS

At least one fluent has a parameter of bounded integer type. Note that unbounded ints are not allowed in fluent parameters).

BOOL_ACTION_PARAMETERS

At least one action has a parameter of boolean type.

BOUNDED_INT_ACTION_PARAMETERS

At least one action has a parameter of bounded integer type.

UNBOUNDED_INT_ACTION_PARAMETERS

At least one action has a parameter of unbounded integer type.

REAL_ACTION_PARAMETERS

At least one action has a parameter of real type.

FLUENTS_TYPE

NUMERIC_FLUENTS

The problem has at least one fluent of numeric type.

OBJECT_FLUENTS

The problem has at least one finite-domain fluent (fluent of user-defined type).

QUALITY_METRICS

ACTIONS_COST

The problem has a quality metric associating a cost to each action and requiring to minimize the total cost of actions used in a plan.

FINAL_VALUE

The problem has a quality metric requiring to optimize the value of a numeric expression in the final state of a plan.

MAKESPAN

The problem has a quality metric requiring to minimize the time at which the last action in the plan is terminated.

PLAN_LENGTH

The problem has a quality metric requiring to minimize the number of actions used in a plan.

OVERSUBSCRIPTION

The problem has a quality metric associating a positive value to some optional goal and the objective is to find the plan of maximal value.

TEMPORAL_OVERSUBSCRIPTION

The problem has a quality metric associating a positive value to some optional timed goal and the objective is to find the plan of maximal value.

SIMULATED_ENTITIES

SIMULATED_EFFECTS

The problem uses at least one simulated effect.

CONSTRAINTS_KIND

TRAJECTORY_CONSTRAINTS

The problem uses at least one LTL trajectory constraint.

STATE_INVARIANTS

The problem uses at least one state invariants.

HIERARCHICAL

METHOD_PRECONDITIONS

At least one method of the problem contains preconditions (i.e. statement that must hold at the start of the method.

TASK_NETWORK_CONSTRAINTS

At least one task network (initial task network or method) contains a constraint: statement over static functions that is required to hold for the task network to appear in the solution.

INITIAL_TASK_NETWORK_VARIABLES

The initial task network contains at least one existentially qualified variable.

TASK_ORDER_TOTAL

In all task networks, all temporal constraints are simple precedence constraints and induce a total order over all subtasks.

TASK_ORDER_PARTIAL

In all task networks, all temporal constraints are simple precedence constraints. At least one task network is not totally ordered.

TASK_ORDER_TEMPORAL

Task networks may be subject to arbitrary temporal constraints (e.g. simple temporal constraints or disjunctive temporal constraints).

The API provides classes and functions to populate a Problem object with the fluents, actions, initial states and goal specifications constituting the planning problem specification. The functionalities for creating model objects and to manipulate them are collected in the unified_planning.model package of the library. In all the examples below all the shortcuts must be imported, with the command:

from unified_planning.shortcuts import *

Classical and Numeric Example

The following example shows a simple robotic planning problem modeling a robot moving between locations while consuming battery. The example shows the basic functionalities and objects needed to declare the problem specification. A more detailed presentation of the different objects is available on the Google Colab Python notebook where we document and explain all the different classes and their semantics.

# Import all the shortcuts, an handy way of using the unified_planning framework
from unified_planning.shortcuts import *

# Declaring types
Location = UserType("Location")

# Creating problem ‘variables’
robot_at = Fluent("robot_at", BoolType(), location=Location)
battery_charge = Fluent("battery_charge", RealType(0, 100))

# Creating actions
move = InstantaneousAction("move", l_from=Location, l_to=Location)
l_from = move.parameter("l_from")
l_to = move.parameter("l_to")
move.add_precondition(GE(battery_charge, 10))
move.add_precondition(robot_at(l_from))
move.add_precondition(Not(robot_at(l_to)))
move.add_effect(robot_at(l_from), False)
move.add_effect(robot_at(l_to), True)
move.add_effect(battery_charge, Minus(battery_charge, 10))

# Declaring objects
l1 = Object("l1", Location)
l2 = Object("l2", Location)

# Populating the problem with initial state and goals
problem = Problem("robot")
problem.add_fluent(robot_at)
problem.add_fluent(battery_charge)
problem.add_action(move)
problem.add_object(l1)
problem.add_object(l2)
problem.set_initial_value(robot_at(l1), True)
problem.set_initial_value(robot_at(l2), False)
problem.set_initial_value(battery_charge, 100)
problem.add_goal(robot_at(l2))

In the current version, the Unified-Planning library allows the specification of classical, numerical and temporal planning problems. In order to support the latitude expressiveness levels we have operators for arithmetic such as plus minus times and division and specific temporal operators to attach conditions and effects to specific timings within the duration of an action. The library documentation provides examples and describes the use of these functionalities.

Temporal Example

Temporal planning is the problem of finding a plan for a planning problem involving durative actions and/or temporal constraints. This means that it is possible to model actions having: * duration constrained by a (possibly state-based) lower and and upper bound * conditions or effects expressed at specific instant of the action, in particular at times start+delay or end+delay, where start refers to the starting time of the action, end to the ending time and delay is a real constant (positive or negative). * durative conditions to be maintained within sub-intervals, delimited by the same time-point used for instantaneous conditions.

problem = Problem("MatchCellar")
Match = UserType("Match")
Fuse = UserType("Fuse")


light = problem.add_fluent("light", BoolType(), default_initial_value=False)
# since BoolType is the default, it can be avoided
handfree = problem.add_fluent("handfree", default_initial_value=True)
match_used = problem.add_fluent("match_used", default_initial_value=False, m=Match)
fuse_mended = problem.add_fluent("fuse_mended", default_initial_value=False, f=Fuse)


light_match = DurativeAction("light_match", m=Match)
m = light_match.parameter("m")
light_match.set_fixed_duration(6)
light_match.add_condition(StartTiming(), Not(match_used(m)))
light_match.add_effect(StartTiming(), match_used(m), True)
light_match.add_effect(StartTiming(), light, True)
light_match.add_effect(EndTiming(), light, False)
problem.add_action(light_match)


mend_fuse = DurativeAction("mend_fuse", f=Fuse)
f = mend_fuse.parameter("f")
mend_fuse.set_fixed_duration(5)
mend_fuse.add_condition(StartTiming(), handfree)
mend_fuse.add_condition(ClosedTimeInterval(StartTiming(), EndTiming()), light)
mend_fuse.add_effect(StartTiming(), handfree, False)
mend_fuse.add_effect(EndTiming(), fuse_mended(f), True)
mend_fuse.add_effect(EndTiming(), handfree, True)
problem.add_action(mend_fuse)


obj_number = 3
match_objects = [Object(f"m{i}", Match) for i in range(1, obj_number + 1)]
problem.add_objects(match_objects)
fuse_objects = [Object(f"f{i}", Fuse) for i in range(1, obj_number + 1)]
problem.add_objects(fuse_objects)


for fuse_obj in fuse_objects:
    problem.add_goal(fuse_mended(fuse_obj))

Scheduling Example

Scheduling is a restricted form of temporal planning where the set of actions (usually called activities) are known in advance and the problem consists in deciding the timing and parameters of the activities. Generally, scheduled problems involve resources and constraints that define the feasible space of solutions. Since scheduling problems are very common and scheduling as a computer science problem belongs to a simpler complexity class (NP) with respect to temporal planning (PSPACE, under suitable assumptions) we created a dedicated representation for scheduling problems. We can represent a generic scheduling problem using the SchedulingProblem class as shown in the example below.

from unified_planning.model.scheduling import SchedulingProblem

problem = SchedulingProblem("factory")
workers = problem.add_resource("workers", capacity=4)
machine1 = problem.add_resource("machine1", capacity=1)
machine2 = problem.add_resource("machine2", capacity=1)

a1 = problem.add_activity("a1", duration=3)
a1.uses(machine1)
a1.uses(workers, amount=2)
a1.add_deadline(14)

a2 = problem.add_activity("a2", duration=6)
a2.uses(workers)
a2.uses(machine2)

problem.add_constraint(LT(a2.end, a1.start))

# One worker is unavailable over [17, 25]
problem.add_decrease_effect(16, workers, 1)
problem.add_increase_effect(25, workers, 1)

MultiAgent Example

Multi-agent Planning lifts planning to a context where many agents operate in a common environment, each with its own view of the domain. The objective is to produce a set of plans, one for each agent, which allows the agents to achieve their goals. The problem comes in several variants, depending on various features. Specifically, multi-agent planning can be competitive, meaning that the agents compete against each other in order to achieve their goal, or cooperative, which refers to the case where the agents collaborate towards a common goal. Another distinction is based on whether planning is performed in a centralized or distributed manner. In the former case, the planning responsibility is assigned to a single entity, which produces a plan consisting of actions, each to be delegated to some agent, while in the latter, the responsibility is distributed to the participating agents, each of which plans at a local level, by possibly exchanging information with the others; in this variant, each agent comes up with a local plan, and the execution of all plans must be appropriately coordinated at runtime. Finally, the setting may or may not require privacy-preservation, which refers to the requirement that every agent might decide not to disclose some information (consequently affecting the space of admissible solutions).

from unified_planning.model.multi_agent import MultiAgentProblem, Agent

problem = MultiAgentProblem("robot_migration")
Location = UserType("Location")
is_connected = Fluent("is_connected", BoolType(), l1=Location, l2=Location)
problem.ma_environment.add_fluent(is_connected, default_initial_value=False)

locations_number = 4
locations = [Object(f"l{i}", Location) for i in range(1, locations_number + 1)]
problem.add_objects(locations)

robot_position = Fluent("pos", Location)

move = InstantaneousAction("move", l_from=Location, l_to=Location)
l_from = move.parameter("l_from")
l_to = move.parameter("l_to")
move.add_precondition(Equals(robot_position, l_from))
move.add_precondition(is_connected(l_from, l_to))
move.add_effect(robot_position, l_to)

robots_number = 3
robots = []
for i in range(1, robots_number + 1):
    robot = Agent(f"robot{i}", problem)
    robots.append(robot)
    robot.add_fluent(robot_position)
    robot.add_action(move)
    problem.add_agent(robot)

last_location = locations[0]
for robot in robots:
    problem.set_initial_value(Dot(robot, robot_position), last_location)

for location in locations[1:]:
    problem.set_initial_value(is_connected(last_location, location), True)
    last_location = location

for robot in robots:
    problem.add_goal(Equals(Dot(robot, robot_position), locations[-1]))

Contingent Example

A contingent planning problem represents an action-based problem in which the exact initial state is not entirely known and some of the actions produce “observations” upon execution. More specifically, some actions can be SensingActions, which indicate which fluents they observe and after the successful execution of such actions, the observed fluents become known to the executor. The inherent non-determinism in the initial state can therefore be “shrinked” by performing suitable SensingActions and a plan is then a strategy that prescribes what to execute based on the past observations.

from unified_planning.model.contingent_problem import ContingentProblem
from unified_planning.model.action import SensingAction

problem = ContingentProblem("lost_packages")
Package = UserType("Package")
Location = UserType("Location")
loader_plate = problem.add_object("loader_plate", Location)
loader_at = problem.add_fluent("loader_at", Location)
is_package_at = problem.add_fluent("is_package_at", location=Location, package=Package)
loader_free = problem.add_fluent("loader_free", default_initial_value=True)

# senses if the package is at the location
sense_package = SensingAction("sense_package", location=Location, package=Package)
sense_package.add_observed_fluent(
    is_package_at(sense_package.location, sense_package.package)
)
sense_package.add_precondition(loader_at.Equals(sense_package.location))

move_loader = InstantaneousAction("move_loader", l_from=Location, l_to=Location)
move_loader.add_precondition(loader_at.Equals(move_loader.l_from))
move_loader.add_effect(loader_at, move_loader.l_to)

load = InstantaneousAction("load", package=Package, location=Location)
load.add_precondition(is_package_at(load.location, load.package))
load.add_precondition(loader_at.Equals(load.location))
load.add_precondition(loader_free)
load.add_effect(is_package_at(load.location, load.package), False)
load.add_effect(is_package_at(loader_plate, load.package), True)
load.add_effect(loader_free, False)

unload = InstantaneousAction("unload", package=Package, location=Location)
unload.add_precondition(is_package_at(loader_plate, unload.package))
unload.add_precondition(loader_at.Equals(unload.location))
unload.add_effect(loader_free, True)
unload.add_effect(is_package_at(loader_plate, unload.package), False)
unload.add_effect(is_package_at(unload.location, unload.package), True)

problem.add_actions([sense_package, move_loader, load, unload])

locations_number = 5
locations = [Object(f"l{i}", Location) for i in range(1, locations_number + 1)]
problem.add_objects(locations)
problem.set_initial_value(loader_at, locations[1])
packages_number = 10
packages = [Object(f"p{i}", Package) for i in range(1, packages_number + 1)]
problem.add_objects(packages)

for p in packages:
    # The package is in only one of the locations
    problem.add_oneof_initial_constraint([is_package_at(l, p) for l in locations])
    problem.set_initial_value(is_package_at(loader_plate, p), False)
    problem.add_goal(is_package_at(locations[-1], p))

Hierarchical Example

A hierarchical planning problem is a problem formulation which extends the Problem object described so far adding support for tasks, methods and decompositions. The general idea is that the planning problem is augmented with high-level tasks that represent abstract operations (e.g. processing an order, going to some distant location) that may require a combination of actions to be achieved. Each high-level task is associated with one or several methods that describe how the task can be decomposed into a set of lower-level tasks and actions.The presence of several methods for a single task represent alternative possibilities of achieving the same operation, among which the planner shall decide. The most important difference between hierarchical and non-hierarchical planning is that in hierarchical planning all actions of the plan must derive from a set of (partially ordered) high-level objective tasks, called the initial task network.

from unified_planning.model.htn import HierarchicalProblem, Method

htn = HierarchicalProblem()

Location = UserType("Location")
l1 = htn.add_object("l1", Location)
l2 = htn.add_object("l2", Location)
l3 = htn.add_object("l3", Location)
l4 = htn.add_object("l4", Location)
l5 = htn.add_object("l5", Location)

loc = htn.add_fluent("loc", Location)

connected = Fluent("connected", l1=Location, l2=Location)
htn.add_fluent(connected, default_initial_value=False)
for li, lj in [(l1, l2), (l2, l3), (l3, l4), (l4, l3), (l3, l2), (l2, l1)]:
    htn.set_initial_value(connected(li, lj), True)

# define low level action, as in non-hierarchical planning
move = InstantaneousAction("move", l_from=Location, l_to=Location)
move.add_precondition(Equals(loc, move.l_from))
move.add_precondition(connected(move.l_from, move.l_to))
move.add_effect(loc, move.l_to)
htn.add_action(move)

# define high-level task: going to some location
go = htn.add_task("go", target=Location)

# first method: nothing to do if already at target
go_noop = Method("go-noop", target=Location)
go_noop.set_task(go)
target = go_noop.parameter("target")
go_noop.add_precondition(Equals(loc, target))
htn.add_method(go_noop)

# second method (tail recursive): first make an allowed move an action
# and recursively decompose go(target) again
go_recursive = Method("go-recursive", source=Location, inter=Location, target=Location)
go_recursive.set_task(go, go_recursive.parameter("target"))
go_recursive.add_precondition(Equals(loc, go_recursive.source))
go_recursive.add_precondition(connected(go_recursive.source, go_recursive.inter))
t1 = go_recursive.add_subtask(
    move, go_recursive.source, go_recursive.inter, ident="move"
)
t2 = go_recursive.add_subtask(go, go_recursive.target, ident="go-rec")
go_recursive.set_ordered(t1, t2)
htn.add_method(go_recursive)

# set objectives tasks in initial task network
go1 = htn.task_network.add_subtask(go, l4, ident="go_l4")
final_loc = htn.task_network.add_variable("final_loc", Location)
go2 = htn.task_network.add_subtask(go, final_loc, ident="go_final")
htn.task_network.add_constraint(Or(Equals(final_loc, l1), Equals(final_loc, l2)))
htn.task_network.set_strictly_before(go1, go2)

htn.set_initial_value(loc, l1)