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 short description, a syntax overview and a link to a detailed discussion.

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).

INT_TYPE_DURATIONS

The duration of at least one action is of int type; added in ProblemKind’s version 2.

REAL_TYPE_DURATIONS

The duration of at least one action is of real type; added in ProblemKind’s version 2.

NUMBERS

CONTINUOUS_NUMBERS

The problem uses numbers ranging over continuous domains (e.g. reals); deprecated in ProblemKind’s version 2.

DISCRETE_NUMBERS

The problem uses numbers ranging over discrete domains (e.g. integers); deprecated in ProblemKind’s version 2.

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; deprecated in ProblemKind’s version 2.

INT_FLUENTS

The problem has at least one fluent of integer type; added in ProblemKind’s version 2.

REAL_FLUENTS

The problem has at least one fluent of real type; added in ProblemKind’s version 2.

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.

ACTIONS_COST_KIND

STATIC_FLUENTS_IN_ACTIONS_COST

There is at least a static fluent in the Action’s cost (that may never change).

FLUENTS_IN_ACTIONS_COST

There is at least a non-static fluent in the Action’s cost (that might change over the course of a plan).

INT_NUMBERS_IN_ACTIONS_COST

There is at least one Action’s cost in the ACTIONS_COST that is of int type; added in ProblemKind’s version 2.

REAL_NUMBERS_IN_ACTIONS_COST

There is at least one Action’s cost in the ACTIONS_COST that is of real type; added in ProblemKind’s version 2.

OVERSUBSCRIPTION_KIND

INT_NUMBERS_IN_OVERSUBSCRIPTION

There is at least one gain in the Oversubscription (or Temporal Oversubscription) metric that is of int type; added in ProblemKind’s version 2.

REAL_NUMBERS_IN_OVERSUBSCRIPTION

There is at least one gain in the Oversubscription (or Temporal Oversubscription) metric that is of real type; added in ProblemKind’s version 2.

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).

MULTI_AGENT

AGENT_SPECIFIC_PRIVATE_GOAL

At least one agent has at least one private specific goal. Private-specific goals are; individual agent goals (not coalition goals) unknown to other agents.

AGENT_SPECIFIC_PUBLIC_GOAL

At least one agent has at least one public-specific goal. Public-specific goals are; individual agent goals (not coalition goals) known to other agents.

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 Planning

Classical and Numeric planning are the most common problems and the building blocks of most other planning problems. At their root, they allow the definition of instantaneous that provide:

  • preconditions that allow to check whether the action is applicable in a given state, and

  • effects that allow the computation of the resulting state after the action is executed.

The classical and numeric planning only differ in that classical planning does not allow to referring to numeric variables in the state. 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. [Classical detailed presentation 🔗] [Numeric detailed presentation 🔗]

Syntax Overview
# 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 Planning

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.

[Detailed presentation 🔗]

Syntax Overview
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))

Hierarchical Planning

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. [Detailed presentation 🔗]

Syntax Overview
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)

Multi-Agent Planning

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). [Detailed presentation 🔗]

Syntax Overview
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 Planning

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.

Syntax Overview
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))

Scheduling

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. [Detailed presentation 🔗]

Syntax Overview
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 [16, 25)
problem.add_decrease_effect(16, workers, 1)
problem.add_increase_effect(25, workers, 1)

Combined Task and Motion Planning

A combined Task and Motion Planning (TAMP) problem allows adding constraints that require that a valid path exists in a map between a series of waypoints given a model of the motion that an object is capable of. This is often important for cases where we need to make sure a motion required by an action is feasible in the real world.

Concretely, the problem is extended by adding an occupancy map with associated configuration types. These types can then be used to add configurations (position and orientation) to objects to form waypoints in the corresponding map. Movable objects contain a footprint and a motion model with its required parameters. In the example below, the Reeds Shepp car is used as a motion model. An instantaneous motion action allows the addition of motion constraints which state that in order to apply the action, a valid path must exist for the movable object between a series of waypoints. [Detailed presentation 🔗]

Syntax Overview
t_robot = MovableType("robot")

occ_map = OccupancyMap(
    os.path.join("../notebooks", "maps", "office-map-1.yaml"), (0, 0)
)

t_robot_config = ConfigurationType("robot_config", occ_map, 3)
t_parcel = UserType("parcel")

robot_at = Fluent("robot_at", BoolType(), robot=t_robot, configuration=t_robot_config)
parcel_at = Fluent(
    "parcel_at", BoolType(), parcel=t_parcel, configuration=t_robot_config
)
carries = Fluent("carries", BoolType(), robot=t_robot, parcel=t_parcel)

park1 = ConfigurationObject("parking-1", t_robot_config, (46.0, 26.0, 3 * math.pi / 2))
park2 = ConfigurationObject("parking-2", t_robot_config, (40.0, 26.0, 3 * math.pi / 2))

office1 = ConfigurationObject("office-1", t_robot_config, (4.0, 4.0, 3 * math.pi / 2))
office2 = ConfigurationObject("office-2", t_robot_config, (14.0, 4.0, math.pi / 2))
office3 = ConfigurationObject("office-3", t_robot_config, (24.0, 4.0, 3 * math.pi / 2))
office4 = ConfigurationObject("office-4", t_robot_config, (32.0, 4.0, 3 * math.pi / 2))
office5 = ConfigurationObject("office-5", t_robot_config, (4.0, 24.0, 3 * math.pi / 2))
office6 = ConfigurationObject("office-6", t_robot_config, (14.0, 24.0, math.pi / 2))
office7 = ConfigurationObject("office-7", t_robot_config, (24.0, 24.0, math.pi / 2))
office8 = ConfigurationObject("office-8", t_robot_config, (32.0, 24.0, math.pi / 2))

r1 = MovableObject(
    "robot-1",
    t_robot,
    footprint=[(-1.0, 0.5), (1.0, 0.5), (1.0, -0.5), (-1.0, -0.5)],
    motion_model=MotionModels.REEDSSHEPP,
    parameters={"turning_radius": 2.0},
)

r2 = MovableObject(
    "robot-2",
    t_robot,
    footprint=[(-1.0, 0.5), (1.0, 0.5), (1.0, -0.5), (-1.0, -0.5)],
    motion_model=MotionModels.REEDSSHEPP,
    parameters={"turning_radius": 2.0},
)

nothing = Object("nothing", t_parcel)
p1 = Object("parcel-1", t_parcel)
p2 = Object("parcel-2", t_parcel)

move = InstantaneousMotionAction(
    "move", robot=t_robot, c_from=t_robot_config, c_to=t_robot_config
)
robot = move.parameter("robot")
c_from = move.parameter("c_from")
c_to = move.parameter("c_to")
move.add_precondition(robot_at(robot, c_from))
move.add_effect(robot_at(robot, c_from), False)
move.add_effect(robot_at(robot, c_to), True)
move.add_motion_constraint(Waypoints(robot, c_from, [c_to]))

pick = InstantaneousMotionAction(
    "pick", robot=t_robot, loc=t_robot_config, parcel=t_parcel
)
pick_robot = pick.parameter("robot")
pick_loc = pick.parameter("loc")
pick_parcel = pick.parameter("parcel")
pick.add_precondition(robot_at(pick_robot, pick_loc))
pick.add_precondition(parcel_at(pick_parcel, pick_loc))
pick.add_precondition(carries(pick_robot, nothing))
pick.add_precondition(Not(carries(pick_robot, pick_parcel)))
pick.add_effect(carries(pick_robot, pick_parcel), True)
pick.add_effect(parcel_at(pick_parcel, pick_loc), False)
pick.add_effect(carries(pick_robot, nothing), False)

place = InstantaneousMotionAction(
    "place", robot=t_robot, loc=t_robot_config, parcel=t_parcel
)
place_robot = place.parameter("robot")
place_loc = place.parameter("loc")
place_parcel = place.parameter("parcel")
place.add_precondition(robot_at(place_robot, place_loc))
place.add_precondition(carries(place_robot, place_parcel))
place.add_precondition(Not(parcel_at(place_parcel, place_loc)))
place.add_precondition(Not(carries(place_robot, nothing)))
place.add_effect(carries(place_robot, place_parcel), False)
place.add_effect(carries(place_robot, nothing), True)
place.add_effect(parcel_at(place_parcel, place_loc), True)

problem = Problem("tamp")
problem.add_fluent(robot_at, default_initial_value=False)
problem.add_fluent(parcel_at, default_initial_value=False)
problem.add_fluent(carries, default_initial_value=False)
problem.add_action(move)
problem.add_action(pick)
problem.add_action(place)

problem.add_objects([park1, park2])
problem.add_objects([office1, office2, office3, office4])
problem.add_objects([office5, office6, office7, office8])
problem.add_objects([r1, r2])
problem.add_object(nothing)
problem.add_objects([p1, p2])

problem.set_initial_value(carries(r1, nothing), True)
problem.set_initial_value(carries(r2, nothing), True)

problem.set_initial_value(parcel_at(p1, office1), True)
problem.set_initial_value(parcel_at(p2, office6), True)

problem.set_initial_value(robot_at(r1, park1), True)

problem.add_goal(robot_at(r1, park1))
problem.add_goal(parcel_at(p1, office2))
problem.add_goal(parcel_at(p2, office3))