Understanding how actions transform states in classical planning
STRIPS is a language for representing actions in classical planning. It defines HOW actions change the world by specifying what must be true before an action can be executed and what changes after execution.
Shakey the Robot
First mobile robot controlled by STRIPS
Before STRIPS, actions were represented as arbitrary procedures (code). STRIPS introduced:
Every STRIPS action consists of exactly three parts:
What must be TRUE before the action can execute
A set of fluents that must exist in the current state. If ANY precondition is missing, the action CANNOT be applied.
Grasp(Box)On(Box, Table), Clear(Box), Empty(Hand)}Precond(a)
What becomes TRUE after the action executes
A set of fluents that will be ADDED to the state. These represent the positive effects of the action.
Grasp(Box)Holding(Box)}Add(a) or Effects+(a)
What becomes FALSE after the action executes
A set of fluents that will be REMOVED from the state. These represent what the action makes false.
Grasp(Box)On(Box, Table), Clear(Box), Empty(Hand)}Del(a) or Effectsβ(a)
The Result function computes the new state after applying an action to the current state.
Current state (set of fluents)
Remove delete list from state
Add add list to result
Tire(Flat, Axle)
Tire(Flat, Axle)
Tire(Flat, Ground)
An airplane flies from one city to another (Saudi Air Cargo context)
At(Plane1, Riyadh)At(Plane1, Jeddah)At(Plane1, Riyadh)Loading cargo into an airplane
At(Cargo1, Riyadh)At(Plane1, Riyadh)In(Cargo1, Plane1)At(Cargo1, Riyadh)A robot moves from one location to another
At(Robot, Kitchen)At(Robot, Bedroom)At(Robot, Kitchen)
So far, we've defined STRIPS actions for specific objects like Remove(Flat, Axle).
But what if we have 10 tires and 5 locations? We'd need 50 different actions!
An Action Schema is a template for actions that uses variables instead of specific objects. One schema can represent many ground actions!
Think of it as a "function" with parameters that can be filled in with different values.
β Works only for "Flat" tire and "Axle" location
Generalize!
β Works for ANY tire and ANY location
Remove(Flat, Axle),
Remove(Spare, Trunk),
Remove(Flat, Ground), ... and more!
?tire, ?loc)? (e.g., ?x, ?robot)One schema instead of hundreds of actions
Same schema works with different objects
This concept is central to PDDL syntax
Try applying STRIPS actions yourself and see the Result function in action!
Tire(Flat, Axle)Tire(Flat, Axle)Tire(Flat, Ground)
Tire(Spare, Trunk)Tire(Spare, Trunk)Tire(Spare, Ground)
Tire(Spare, Ground), Β¬Tire(Flat, Axle)Tire(Spare, Ground)Tire(Spare, Axle)
Notice that PutOn(Spare, Axle) has a negative precondition: Β¬Tire(Flat, Axle).
This ensures the axle is empty before putting the spare on it. Under the Closed-World Assumption (CWA),
we check that Tire(Flat, Axle) is NOT in the state. This prevents physically impossible situations
like having two tires on the same axle!
On(Box, Table), Clear(Box), Empty(Hand)On(Box, Table), Clear(Box), Empty(Hand)Holding(Box)
Holding(Box)Holding(Box)On(Box, Target), Clear(Box), Empty(Hand)
You can represent STRIPS models (states, actions, goals) programmatically in Python using scoped classes and objects β without relying on PDDL files!
This approach gives you full control over the planning process and helps you understand the mechanics of STRIPS at a deeper level.
Each STRIPS action is represented as a Python class with preconditions, add list, and delete list:
class Action:
def __init__(self, name, precond, add, delete):
"""Initialize a STRIPS action"""
self.name = name
self.precond = set(precond) # Preconditions (must be true)
self.add = set(add) # Add list (becomes true)
self.delete = set(delete) # Delete list (becomes false)
def applicable(self, state):
"""Check if action can be applied to the given state"""
return self.precond.issubset(state)
def apply(self, state):
"""Apply action: Result(s, a) = (s - Del(a)) βͺ Add(a)"""
return (state - self.delete) | self.add
def __repr__(self):
return self.name
applicable(state) - Checks if preconditions are satisfiedapply(state) - Implements the STRIPS Result function: (s β Del) βͺ AddEncapsulate the initial state, goal, and actions in a problem class:
class STRIPSProblem:
def __init__(self, initial, goal, actions):
"""Initialize a STRIPS planning problem"""
self.initial = set(initial) # Initial state (set of facts)
self.goal = set(goal) # Goal state (set of facts)
self.actions = actions # List of available actions
def goal_reached(self, state):
"""Check if goal is satisfied in the given state"""
return self.goal.issubset(state)
def get_applicable_actions(self, state):
"""Get all actions applicable in the given state"""
return [a for a in self.actions if a.applicable(state)]
Let's put it all together with the classic Blocks World problem:
# Define initial state
initial_state = {
"ontable(A)",
"ontable(B)",
"clear(A)",
"clear(B)",
"handempty"
}
# Define goal
goal = {"on(A,B)"}
# Define actions
actions = [
Action(
"pick-up(A)",
precond={"ontable(A)", "clear(A)", "handempty"},
add={"holding(A)"},
delete={"ontable(A)", "clear(A)", "handempty"}
),
Action(
"pick-up(B)",
precond={"ontable(B)", "clear(B)", "handempty"},
add={"holding(B)"},
delete={"ontable(B)", "clear(B)", "handempty"}
),
Action(
"stack(A,B)",
precond={"holding(A)", "clear(B)"},
add={"on(A,B)", "clear(A)", "handempty"},
delete={"holding(A)", "clear(B)"}
),
Action(
"stack(B,A)",
precond={"holding(B)", "clear(A)"},
add={"on(B,A)", "clear(B)", "handempty"},
delete={"holding(B)", "clear(A)"}
)
]
# Create problem instance
problem = STRIPSProblem(initial_state, goal, actions)
# Test: Check what actions are applicable
print("Initial state:", problem.initial)
print("Applicable actions:", problem.get_applicable_actions(problem.initial))
Now let's implement a simple forward search to find a plan:
from collections import deque
def forward_search(problem, max_depth=10):
"""
Simple breadth-first forward search planner
Returns: list of action names forming a plan, or None if no plan found
"""
# Queue stores: (current_state, plan_so_far)
frontier = deque([(problem.initial, [])])
visited = {frozenset(problem.initial)}
nodes_expanded = 0
while frontier:
state, plan = frontier.popleft()
nodes_expanded += 1
# Goal test
if problem.goal_reached(state):
print(f"β
Solution found!")
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
# Expand node: try all applicable actions
for action in problem.get_applicable_actions(state):
new_state = action.apply(state)
state_signature = frozenset(new_state)
if state_signature not in visited:
visited.add(state_signature)
new_plan = plan + [action.name]
frontier.append((new_state, new_plan))
print("β No solution found")
return None
# Run the planner
print("\nπ Searching for a plan...")
print("=" * 50)
plan = forward_search(problem)
if plan:
print("\nπ Plan Found:")
for i, action_name in enumerate(plan, 1):
print(f" {i}. {action_name}")
# Verify by executing the plan
print("\n㪠Executing plan step-by-step:")
state = problem.initial
print(f"Initial: {state}")
for action_name in plan:
action = next(a for a in problem.actions if a.name == action_name)
state = action.apply(state)
print(f"After {action_name}: {state}")
print(f"\nπ― Goal {'β
ACHIEVED' if problem.goal_reached(state) else 'β NOT ACHIEVED'}!")
π Searching for a plan...
==================================================
β
Solution found!
π Nodes expanded: 3
π Plan length: 2
π Plan Found:
1. pick-up(A)
2. stack(A,B)
π¬ Executing plan step-by-step:
Initial: {'ontable(A)', 'ontable(B)', 'clear(A)', 'clear(B)', 'handempty'}
After pick-up(A): {'ontable(B)', 'clear(B)', 'holding(A)'}
After stack(A,B): {'ontable(B)', 'on(A,B)', 'clear(A)', 'handempty'}
π― Goal β
ACHIEVED!
Each action is scoped in its own class with encapsulated behavior
Easy to extend with subclasses (e.g., hierarchical planning)
Full control over search strategies and heuristics
pddlpy to load PDDL files and manipulate them in Python.
Beyond building from scratch, there are several Python libraries that implement or support STRIPS-style planning:
pyddl - Pure Python STRIPS Library# Install
pip install pyddl
# Example code
from pyddl import Domain, Problem, Action, planner
domain = Domain((
Action('pick-up', parameters=('x',),
preconditions=(('ontable', 'x'), ('clear', 'x'), ('handempty',)),
effects=(('holding', 'x'), ('not', ('ontable', 'x')))
),
))
problem = Problem(domain, {'A': 'block'},
init=(('ontable', 'A'), ('clear', 'A'), ('handempty',)),
goal=(('holding', 'A'),)
)
plan = planner(problem)
# Output: [pick-up(A)]
pddlpy - PDDL Parser# Install
pip install pddlpy
# Example code
from pddlpy import DomainProblem
dp = DomainProblem('blocks-domain.pddl', 'blocks-problem.pddl')
# Check applicable actions
for act in dp.ground_operator():
if dp.is_applicable(act):
print("β
Applicable:", act)
# Manually apply action
dp.apply_operator(('pick-up', 'A'))
print("State after action:", dp.state)
pyperplan - Educational STRIPS Planner# Install
pip install pyperplan
# Example code
from pyperplan.pddl.parser import Parser
from pyperplan.planner import _ground, _search
parser = Parser('blocks-domain.pddl', 'blocks-problem.pddl')
domain, problem = parser.parse_domain_problem()
task = _ground(domain, problem)
plan = _search(task, heuristic='hff') # FF heuristic
for step in plan:
print(f"β
{step}")
unified-planning - Modern Unified Framework# Install
pip install unified-planning[pyperplan]
# Example code
from unified_planning.shortcuts import *
problem = Problem("blocks")
Block = UserType("Block")
A = Object("A", Block)
ontable = Fluent("ontable", BoolType(), x=Block)
clear = Fluent("clear", BoolType(), x=Block)
pickup = InstantaneousAction("pickup", x=Block)
pickup.add_precondition(ontable(pickup.x))
pickup.add_effect(clear(pickup.x), False)
problem.add_action(pickup)
problem.set_initial_value(ontable(A), True)
problem.add_goal(Not(ontable(A)))
with OneshotPlanner(name="pyperplan") as planner:
result = planner.solve(problem)
print(result.plan)
| Library | Type | Supports PDDL | Planning | Strength |
|---|---|---|---|---|
| pyddl | Pure Python STRIPS | β No | β Yes | Simple, clean, ideal for teaching |
| pddlpy | PDDL parser | β Yes | β No | Good for reasoning/inference |
| pyperplan | Academic STRIPS planner | β Yes | β Yes | Teachable A* STRIPS planner |
| unified-planning | Modern unified framework | β Yes | β Yes | Industrial-grade, modular, powerful |
Start with pyddl β everything stays in Python, no PDDL files, very clear to read.
Use pddlpy β connects Python with classical planning theory and shows how PDDL is parsed.
Try pyperplan β demonstrates heuristics (A*, FF) in a teachable implementation.
pyddl for quick prototypes,
pddlpy to load standard PDDL benchmarks, and pyperplan
to experiment with different search strategies and heuristics. See Topic 09 for
complete examples of all three approaches!