From PDDL representation to automated reasoning with Python
Now that you understand planning theory, let's see how to implement it in Python!
We'll use PDDL for representation (domain & problem files) and Python for automated reasoning (finding plans programmatically).
Declarative problem specification
Automated reasoning & search
Executable action sequences
We'll explore two ways to implement planning in Python:
This is a comprehensive reference of PDDL syntax and commands. Use this as a quick guide when writing your own domain and problem files!
(define (domain domain-name)
(:requirements :strips :typing :negative-preconditions)
(:types
type1 type2 - object
)
(:predicates
(predicate-name ?param1 - type1 ?param2 - type2)
)
(:action action-name
:parameters (?var1 - type1 ?var2 - type2)
:precondition (and
(predicate1 ?var1)
(predicate2 ?var1 ?var2)
)
:effect (and
(new-predicate ?var1)
(not (old-predicate ?var2))
)
)
)
(define (problem problem-name)
(:domain domain-name)
(:objects
obj1 obj2 - type1
obj3 obj4 - type2
)
(:init
(predicate1 obj1)
(predicate2 obj1 obj2)
)
(:goal (and
(goal-predicate1 obj3)
(goal-predicate2 obj3 obj4)
))
)
| Command | Purpose | Example |
|---|---|---|
define |
Start domain or problem definition | (define (domain robots)) |
:requirements |
Specify PDDL features needed | :strips :typing |
:types |
Define object types | robot location - object |
:predicates |
Define state predicates | (at ?r - robot ?l - location) |
:action |
Define an action schema | (:action move ...) |
:parameters |
Action parameters (variables) | (?r - robot ?from ?to - location) |
:precondition |
Conditions that must be true | (at ?r ?from) |
:effect |
Changes to state | (and (at ?r ?to) (not (at ?r ?from))) |
:objects |
Define problem objects | robot1 - robot kitchen - location |
:init |
Initial state facts | (at robot1 kitchen) |
:goal |
Goal condition to achieve | (at robot1 bedroom) |
| Operator | Meaning | Example |
|---|---|---|
and |
Logical AND (all must be true) | (and (p) (q)) |
or |
Logical OR (at least one true) | (or (p) (q)) |
not |
Logical NOT (negation) | (not (p)) |
imply |
Logical implication | (imply (p) (q)) |
:disjunctive-preconditions? (e.g., ?robot)kitchen)| Requirement | Description |
|---|---|
:strips |
Basic STRIPS-style planning (preconditions, add/delete effects) |
:typing |
Allows typed objects and parameters |
:negative-preconditions |
Allows (not ...) in preconditions |
:disjunctive-preconditions |
Allows (or ...) in preconditions |
:equality |
Allows (= ?x ?y) for testing equality |
:conditional-effects |
Effects can be conditional: (when (p) (q)) |
:fluents |
Numeric fluents and functions |
:adl |
Action Description Language (includes many features) |
(:action move
; Parameters: variables that will be instantiated
:parameters (?robot - robot
?from - location
?to - location)
; Preconditions: must ALL be true for action to execute
:precondition (and
(at ?robot ?from) ; robot is at 'from' location
(connected ?from ?to) ; locations are connected
(not (blocked ?to)) ; destination is not blocked
)
; Effects: changes to the state
:effect (and
(at ?robot ?to) ; ADD: robot is now at 'to'
(not (at ?robot ?from)) ; DELETE: robot no longer at 'from'
(visited ?to) ; ADD: mark location as visited
)
)
?- typenameand, or, not(not ...); Pick up object
(:action pick
:parameters (?obj - object)
:precondition (and
(at-hand empty)
(at-obj ?obj table)
)
:effect (and
(holding ?obj)
(not (at-hand empty))
(not (at-obj ?obj table))
)
)
; Place object
(:action place
:parameters (?obj - object)
:precondition (holding ?obj)
:effect (and
(at-obj ?obj table)
(at-hand empty)
(not (holding ?obj))
)
)
; Move between locations
(:action drive
:parameters (?v - vehicle
?from ?to - location)
:precondition (and
(at ?v ?from)
(road ?from ?to)
(has-fuel ?v)
)
:effect (and
(at ?v ?to)
(not (at ?v ?from))
)
)
; Refuel vehicle
(:action refuel
:parameters (?v - vehicle
?loc - location)
:precondition (and
(at ?v ?loc)
(has-pump ?loc)
)
:effect (has-fuel ?v)
)
Before running your PDDL files, verify:
? prefix:types
This reference shows how to use Python to load, manipulate, and reason about PDDL planning problems using the pddlpy library.
from pddlpy import DomainProblem
# Load domain and problem files
domain_file = "domain.pddl"
problem_file = "problem.pddl"
# Create DomainProblem object
dp = DomainProblem(domain_file, problem_file)
# The dp object now contains parsed PDDL information
print(f"Domain: {dp.domain_name}")
print(f"Problem: {dp.problem_name}")
| Method | Purpose | Example |
|---|---|---|
dp.ground_operator() |
Get all ground actions (instantiated) | actions = list(dp.ground_operator()) |
dp.is_applicable(action) |
Check if action can be executed in current state | if dp.is_applicable(action): |
dp.apply_operator(action) |
Execute action and update state | dp.apply_operator(('move', 'A', 'B')) |
dp.check_goal() |
Check if current state satisfies goal | if dp.check_goal(): print("Goal!") |
dp.copy() |
Create deep copy of current state | new_dp = dp.copy() |
dp.state |
Access current state (set of fluents) | current_fluents = dp.state |
dp.initial_state |
Access initial state | init = dp.initial_state |
dp.goals |
Access goal conditions | goal_fluents = dp.goals |
dp.operators() |
Get action schemas (uninstantiated) | schemas = dp.operators() |
dp.worldobjects() |
Get all objects in the problem | objects = dp.worldobjects() |
from pddlpy import DomainProblem
dp = DomainProblem('domain.pddl', 'problem.pddl')
# ===== Accessing State =====
# Get current state (set of fluents)
current_state = dp.state
print("Current state:", current_state)
# Get initial state
initial = dp.initial_state
print("Initial state:", initial)
# Get goal conditions
goals = dp.goals
print("Goals:", goals)
# ===== Checking Predicates =====
# Check if a specific fluent is in current state
if ('at', 'robot1', 'kitchen') in dp.state:
print("Robot is in kitchen!")
# ===== Working with Actions =====
# Get all possible ground actions
all_actions = list(dp.ground_operator())
print(f"Total actions: {len(all_actions)}")
# Find applicable actions in current state
applicable = [a for a in all_actions if dp.is_applicable(a)]
print(f"Applicable actions: {len(applicable)}")
# ===== Applying Actions =====
# Apply an action (modifies state in-place)
action = ('move', 'robot1', 'kitchen', 'bedroom')
if dp.is_applicable(action):
dp.apply_operator(action)
print(f"Applied action: {action}")
print(f"New state: {dp.state}")
# ===== Goal Checking =====
# Check if goal is satisfied
if dp.check_goal():
print("ā
Goal achieved!")
else:
print("ā Goal not yet achieved")
# ===== Copying State =====
# Create a copy to explore without modifying original
dp_copy = dp.copy()
dp_copy.apply_operator(some_action) # Won't affect dp
Actions are represented as tuples:
# Structure: (action_name, param1, param2, ...)
# Examples:
action1 = ('move', 'robot1', 'kitchen', 'bedroom')
action2 = ('pick-up', 'blockA')
action3 = ('stack', 'blockA', 'blockB')
action4 = ('load', 'cargo1', 'plane1', 'airport1')
# Accessing components:
action_name = action1[0] # 'move'
parameters = action1[1:] # ('robot1', 'kitchen', 'bedroom')
Fluents are also tuples:
# Structure: (predicate_name, obj1, obj2, ...)
# Examples:
fluent1 = ('at', 'robot1', 'kitchen')
fluent2 = ('ontable', 'blockA')
fluent3 = ('on', 'blockA', 'blockB')
fluent4 = ('clear', 'blockC')
fluent5 = ('handempty',) # Note the comma!
# Checking fluents:
if fluent1 in dp.state:
print("Fluent is true!")
def get_applicable_actions(dp):
"""Get all actions that can be executed in current state"""
all_actions = dp.ground_operator()
applicable = []
for action in all_actions:
if dp.is_applicable(action):
applicable.append(action)
return applicable
# Usage
applicable = get_applicable_actions(dp)
print(f"Can execute {len(applicable)} actions")
for action in applicable:
print(f" - {action[0]}({', '.join(action[1:])})")
def simulate_plan(dp, plan):
"""Simulate execution of a plan and show state after each action"""
print("Initial state:")
print(f" {dp.state}\n")
for i, action in enumerate(plan, 1):
if not dp.is_applicable(action):
print(f"ā Action {i} is not applicable: {action}")
return False
print(f"Step {i}: {action[0]}({', '.join(action[1:])})")
dp.apply_operator(action)
print(f" State: {dp.state}\n")
if dp.check_goal():
print("ā
Goal achieved!")
return True
else:
print("ā Goal not achieved")
return False
# Usage
plan = [
('pick-up', 'blockA'),
('stack', 'blockA', 'blockB')
]
simulate_plan(dp, plan)
def check_condition(dp, condition):
"""Check if a condition (fluent) is true in current state"""
return condition in dp.state
def check_all_conditions(dp, conditions):
"""Check if all conditions are true (AND)"""
return all(cond in dp.state for cond in conditions)
def check_any_condition(dp, conditions):
"""Check if any condition is true (OR)"""
return any(cond in dp.state for cond in conditions)
# Usage
if check_condition(dp, ('at', 'robot1', 'kitchen')):
print("Robot is in kitchen")
conditions = [('clear', 'blockA'), ('ontable', 'blockB')]
if check_all_conditions(dp, conditions):
print("Both blocks are ready")
def print_state_pretty(dp):
"""Pretty print the current state grouped by predicate"""
from collections import defaultdict
grouped = defaultdict(list)
for fluent in dp.state:
predicate = fluent[0]
args = fluent[1:]
grouped[predicate].append(args)
print("Current State:")
for predicate, args_list in sorted(grouped.items()):
print(f" {predicate}:")
for args in args_list:
if args:
print(f" - {', '.join(args)}")
else:
print(f" - (no arguments)")
# Usage
print_state_pretty(dp)
# Output example:
# Current State:
# at:
# - robot1, kitchen
# - blockA, table
# clear:
# - blockA
# - blockB
# handempty:
# - (no arguments)
from collections import deque
from pddlpy import DomainProblem
def bfs_planner(domain_file, problem_file, max_depth=10):
"""Simple breadth-first search planner using pddlpy"""
# Load problem
dp = DomainProblem(domain_file, problem_file)
# Initialize search
queue = deque([(dp, [])]) # (state, plan)
visited = {frozenset(dp.state)}
nodes_expanded = 0
while queue:
current_dp, plan = queue.popleft()
nodes_expanded += 1
# Goal test
if current_dp.check_goal():
print(f"ā
Solution found!")
print(f"š Nodes expanded: {nodes_expanded}")
print(f"š Plan length: {len(plan)}")
return plan
# Depth limit
if len(plan) >= max_depth:
continue
# Expand node
for action in current_dp.ground_operator():
if current_dp.is_applicable(action):
# Create successor
new_dp = current_dp.copy()
new_dp.apply_operator(action)
# Check if visited
state_sig = frozenset(new_dp.state)
if state_sig not in visited:
visited.add(state_sig)
new_plan = plan + [action]
queue.append((new_dp, new_plan))
print("ā No solution found")
return None
# Usage
plan = bfs_planner('blocks-domain.pddl', 'blocks-problem.pddl')
if plan:
print("\nš Plan:")
for i, action in enumerate(plan, 1):
print(f" {i}. {action[0]}({', '.join(action[1:])})")
is_applicable() before apply_operator()copy() when exploring branches# Load
from pddlpy import DomainProblem
dp = DomainProblem('d.pddl', 'p.pddl')
# Access info
dp.state # Current state
dp.initial_state # Initial state
dp.goals # Goal conditions
# Actions
actions = list(dp.ground_operator())
if dp.is_applicable(action):
dp.apply_operator(action)
# Goals
if dp.check_goal():
print("Done!")
We'll use the classic Blocks World domain where a robot manipulates blocks.
Goal: Stack block A on top of block B
File: blocks-domain.pddl
(define (domain blocks)
(:requirements :strips)
(:predicates
(on ?x ?y) ; block x is on block y
(ontable ?x) ; block x is on the table
(clear ?x) ; nothing is on top of block x
(holding ?x) ; robot is holding block x
(handempty) ; robot's hand is empty
)
(:action pick-up
:parameters (?x)
:precondition (and (ontable ?x) (clear ?x) (handempty))
:effect (and (holding ?x)
(not (ontable ?x))
(not (clear ?x))
(not (handempty)))
)
(:action put-down
:parameters (?x)
:precondition (holding ?x)
:effect (and (ontable ?x)
(clear ?x)
(handempty)
(not (holding ?x)))
)
(:action stack
:parameters (?x ?y)
:precondition (and (holding ?x) (clear ?y))
:effect (and (on ?x ?y)
(clear ?x)
(handempty)
(not (holding ?x))
(not (clear ?y)))
)
(:action unstack
:parameters (?x ?y)
:precondition (and (on ?x ?y) (clear ?x) (handempty))
:effect (and (holding ?x)
(clear ?y)
(not (on ?x ?y))
(not (clear ?x))
(not (handempty)))
)
)
File: blocks-problem.pddl
(define (problem stack-blocks)
(:domain blocks)
(:objects A B)
(:init
(ontable A) ; A is on the table
(ontable B) ; B is on the table
(clear A) ; A is clear
(clear B) ; B is clear
(handempty) ; robot's hand is empty
)
(:goal (on A B)) ; Goal: A is on top of B
)
Install pddlpy using pip:
File: planner.py
from pddlpy import DomainProblem
# Load the domain and problem files
dp = DomainProblem('blocks-domain.pddl', 'blocks-problem.pddl')
print("š Initial State Reasoning:")
print("=" * 50)
# Get all ground operators (actions with specific parameters)
all_actions = list(dp.ground_operator())
print(f"Total possible actions: {len(all_actions)}")
# Find applicable actions in initial state
applicable = [a for a in all_actions if dp.is_applicable(a)]
print(f"\nā
Applicable actions in initial state:")
for act in applicable:
print(f" ⢠{act}")
# Simple forward search to find a plan
def search_plan(dp, max_depth=5):
"""Simple breadth-first search for a plan"""
from collections import deque
# Queue stores: (domain_problem_state, plan_so_far)
queue = deque([(dp, [])])
visited = set()
while queue:
current_dp, plan = queue.popleft()
# Check if goal is reached
if current_dp.check_goal():
return plan
# Get state signature for cycle detection
state_sig = frozenset(current_dp.state)
if state_sig in visited or len(plan) >= max_depth:
continue
visited.add(state_sig)
# Try all applicable actions
for action in current_dp.ground_operator():
if current_dp.is_applicable(action):
# Create new state by applying action
new_dp = current_dp.copy()
new_dp.apply_operator(action)
new_plan = plan + [action]
queue.append((new_dp, new_plan))
return None # No plan found
print("\nš§ Searching for a plan...")
print("=" * 50)
plan = search_plan(dp)
if plan:
print(f"\nš Plan found with {len(plan)} actions:")
for i, action in enumerate(plan, 1):
print(f" Step {i}: {action[0]}({', '.join(action[1:])})")
else:
print("\nā No plan found!")
# Verify the plan by applying it
print("\nš¬ Verifying plan execution:")
print("=" * 50)
for i, action in enumerate(plan, 1):
print(f"Step {i}: Applying {action}")
dp.apply_operator(action)
if dp.check_goal():
print("\nā
Goal achieved successfully!")
else:
print("\nā Goal not achieved!")
Execute the planner:
Expected Output:
Now let's implement the same problem using pure Python without any external libraries. This helps understand the underlying mechanics of automated planning!
File: python_planner.py
from collections import deque
from copy import deepcopy
# ========== State Representation ==========
class BlocksWorld:
"""Represents the state of the blocks world"""
def __init__(self):
self.ontable = set() # blocks on the table
self.clear = set() # blocks with nothing on top
self.on = {} # on[x] = y means x is on y
self.holding = None # block currently held (or None)
def copy(self):
"""Create a deep copy of the state"""
new_state = BlocksWorld()
new_state.ontable = self.ontable.copy()
new_state.clear = self.clear.copy()
new_state.on = self.on.copy()
new_state.holding = self.holding
return new_state
def __str__(self):
"""String representation of the state"""
lines = []
lines.append(f"On table: {sorted(self.ontable)}")
lines.append(f"Clear: {sorted(self.clear)}")
lines.append(f"On relations: {self.on}")
lines.append(f"Holding: {self.holding}")
return "\n".join(lines)
def signature(self):
"""Get a hashable signature for state comparison"""
return (
frozenset(self.ontable),
frozenset(self.clear),
frozenset(self.on.items()),
self.holding
)
# ========== Action Definitions ==========
class BlocksActions:
"""Defines all possible actions in blocks world"""
@staticmethod
def can_pickup(state, x):
"""Check if we can pick up block x"""
return (x in state.ontable and
x in state.clear and
state.holding is None)
@staticmethod
def pickup(state, x):
"""Pick up block x from the table"""
new_state = state.copy()
new_state.ontable.remove(x)
new_state.clear.remove(x)
new_state.holding = x
return new_state
@staticmethod
def can_putdown(state, x):
"""Check if we can put down block x"""
return state.holding == x
@staticmethod
def putdown(state, x):
"""Put down block x on the table"""
new_state = state.copy()
new_state.ontable.add(x)
new_state.clear.add(x)
new_state.holding = None
return new_state
@staticmethod
def can_stack(state, x, y):
"""Check if we can stack block x on block y"""
return state.holding == x and y in state.clear
@staticmethod
def stack(state, x, y):
"""Stack block x on block y"""
new_state = state.copy()
new_state.on[x] = y
new_state.clear.add(x)
new_state.clear.remove(y)
new_state.holding = None
return new_state
@staticmethod
def can_unstack(state, x, y):
"""Check if we can unstack block x from block y"""
return (x in state.on and
state.on[x] == y and
x in state.clear and
state.holding is None)
@staticmethod
def unstack(state, x, y):
"""Unstack block x from block y"""
new_state = state.copy()
del new_state.on[x]
new_state.clear.add(y)
new_state.clear.remove(x)
new_state.holding = x
return new_state
# ========== Planner ==========
class Planner:
"""Simple forward search planner"""
def __init__(self, initial_state, goal_test, actions_class):
self.initial_state = initial_state
self.goal_test = goal_test
self.actions = actions_class
def get_applicable_actions(self, state, blocks):
"""Get all applicable actions in current state"""
applicable = []
for x in blocks:
if self.actions.can_pickup(state, x):
applicable.append(('pickup', x))
if self.actions.can_putdown(state, x):
applicable.append(('putdown', x))
for y in blocks:
if x != y:
if self.actions.can_stack(state, x, y):
applicable.append(('stack', x, y))
if self.actions.can_unstack(state, x, y):
applicable.append(('unstack', x, y))
return applicable
def apply_action(self, state, action):
"""Apply an action to a state"""
action_name = action[0]
if action_name == 'pickup':
return self.actions.pickup(state, action[1])
elif action_name == 'putdown':
return self.actions.putdown(state, action[1])
elif action_name == 'stack':
return self.actions.stack(state, action[1], action[2])
elif action_name == 'unstack':
return self.actions.unstack(state, action[1], action[2])
return None
def search(self, blocks, max_depth=10):
"""Breadth-first search for a plan"""
queue = deque([(self.initial_state, [])])
visited = {self.initial_state.signature()}
nodes_expanded = 0
while queue:
current_state, plan = queue.popleft()
nodes_expanded += 1
# Check if goal is reached
if self.goal_test(current_state):
print(f"\nš Search Statistics:")
print(f" Nodes expanded: {nodes_expanded}")
print(f" Plan length: {len(plan)}")
return plan
# Don't search beyond max depth
if len(plan) >= max_depth:
continue
# Try all applicable actions
for action in self.get_applicable_actions(current_state, blocks):
new_state = self.apply_action(current_state, action)
state_sig = new_state.signature()
if state_sig not in visited:
visited.add(state_sig)
new_plan = plan + [action]
queue.append((new_state, new_plan))
return None # No plan found
# ========== Main Program ==========
def main():
print("š§± Blocks World Planner")
print("=" * 60)
# Define initial state
initial = BlocksWorld()
initial.ontable = {'A', 'B'}
initial.clear = {'A', 'B'}
initial.on = {}
initial.holding = None
print("\nš¦ Initial State:")
print(initial)
print("\nšÆ Goal: Stack A on B (i.e., A is on B)")
# Define goal test
def goal_test(state):
return 'A' in state.on and state.on['A'] == 'B'
# Create planner
planner = Planner(initial, goal_test, BlocksActions)
# Search for a plan
print("\nš Searching for a plan...")
plan = planner.search(['A', 'B'])
if plan:
print(f"\nā
Plan found with {len(plan)} actions:")
for i, action in enumerate(plan, 1):
action_str = f"{action[0]}({', '.join(action[1:])})"
print(f" Step {i}: {action_str}")
# Simulate execution
print("\nš¬ Simulating plan execution:")
print("=" * 60)
state = initial
for i, action in enumerate(plan, 1):
print(f"\nStep {i}: {action[0]}({', '.join(action[1:])})")
state = planner.apply_action(state, action)
print(state)
if goal_test(state):
print("\nš Goal achieved successfully!")
else:
print("\nā Goal not achieved!")
else:
print("\nā No plan found!")
if __name__ == "__main__":
main()
Output:
| Feature | PDDL + pddlpy | Pure Python |
|---|---|---|
| Dependencies | pddlpy library | None |
| Code Lines | ~70 (Python) + PDDL files | ~200 (all Python) |
| Flexibility | Medium | High |
| Standard Format | ā Yes (PDDL) | ā No |
| Learning Curve | Medium (PDDL syntax) | Low (just Python) |
| Reusability | High (PDDL files) | Medium (Python code) |
| Best For | Production systems | Learning & prototyping |
Standard, declarative format
Flexible, programmable reasoning
Let computers find solutions!