====== 10. Self-Contained PDDL Modeling with Unified Planning ======
This lab introduces classical planning modeling directly in Python with [[https://github.com/aiplan4eu/unified-planning|Unified Planning]]. Students construct two planning problems, solve them from in-memory objects, and optionally export the same models to PDDL.
===== Learning Goals =====
* Build a planning model with ''Problem'', ''Fluent'', ''InstantaneousAction'', objects, initial facts, and goals.
* Understand how domain rules are translated into operators with preconditions and effects.
* Solve a planning problem directly from Python objects without first loading PDDL files.
* Export the generated model to ''domain.pddl'' and ''problem.pddl''.
===== Setup =====
Run the following cell first to install the required Python packages:
%pip install \
"unified-planning>=1.1.0" \
"unified-planning[engines]>=1.1.0" \
"pddl>=0.4.3"
The notebook utilities are:
from pathlib import Path
from unified_planning.io import PDDLWriter
from unified_planning.shortcuts import (
And,
BoolType,
Fluent,
InstantaneousAction,
Object,
OneshotPlanner,
Problem,
UserType,
)
ROOT = Path.cwd()
DOMAINS_DIR = ROOT / "domains"
DOMAINS_DIR.mkdir(parents=True, exist_ok=True)
def solve_problem(problem: Problem, engine: str | None = None):
planner_kwargs = {}
if engine is not None:
planner_kwargs["name"] = engine
with OneshotPlanner(problem_kind=problem.kind, **planner_kwargs) as planner:
return planner.solve(problem)
def export_pddl(problem: Problem, out_dir: Path):
out_dir.mkdir(parents=True, exist_ok=True)
writer = PDDLWriter(problem)
writer.write_domain(str(out_dir / "domain.pddl"))
writer.write_problem(str(out_dir / "problem.pddl"))
===== Student Tasks =====
The student notebook uses a gradual modeling workflow. For each domain, students:
- keep the provided types and fluents,
- complete operator preconditions and effects,
- complete the objects, initial state, and goal,
- run the model and inspect the produced plan.
The first action in each domain is already implemented as a worked example. The remaining actions and missing facts are marked with ''TODO'' comments.
===== Domain 1: Perestroika =====
==== Modeling Idea ====
The agent moves across locations, collects resources, and must stay alive while tiles shrink and disappear. The model tracks accessibility, occupancy, and tile level changes.
==== Main Fluents ====
^ Fluent ^ Meaning ^
| ''at-agent(l)'' | The player is at location ''l''. |
| ''connected(l1, l2)'' | The two locations are adjacent. |
| ''at-res(res, l)'' | Resource ''res'' is placed at location ''l''. |
| ''taken(res)'' | The resource has been collected. |
| ''accessible(l)'' | The tile can still be entered. |
| ''alive'' / ''dead'' | Survival state of the agent. |
| ''level(l, lvl)'' | Current shrinking level of a tile. |
| ''next(lvl1, lvl2)'' | Successor relation between levels. |
| ''level-max(l, lvl)'' | Maximum regeneration level of a tile. |
| ''level-min(lvl)'' | Minimum level in the shrinking chain. |
| ''free(l)'' | The location is currently unoccupied. |
==== Main Actions ====
* ''move'': move between connected and accessible locations.
* ''collect'': pick up a resource at the current position.
* ''shrink'': reduce a tile level from ''lvl2'' to ''lvl1''.
* ''shrink-small-empty'': remove an empty tile at minimum level.
* ''shrink-small-agent'': remove a minimum-level tile while the agent is standing on it, killing the agent.
* ''create'': regenerate a missing tile at its maximum level.
==== Reference Goal ====
perestroika_problem.add_goal(
And(
perestroika_fluents["alive"](),
perestroika_fluents["taken"](c1),
perestroika_fluents["taken"](c2),
)
)
===== Domain 2: AUV =====
==== Modeling Idea ====
The AUV moves through underwater locations, collects samples, and interacts with a ship that can occupy locations and cause destructive collisions if modeled incorrectly.
==== Main Fluents ====
^ Fluent ^ Meaning ^
| ''at(v, l)'' | Vehicle ''v'' is at location ''l''. |
| ''connected(l1, l2)'' | AUV movement edge between locations. |
| ''at-res(r, l)'' | Sample ''r'' is located at ''l''. |
| ''sampled(r)'' | The sample has been collected. |
| ''free(l)'' | The location is free. |
| ''operational(a)'' | The AUV is still functioning. |
| ''connected-ship(s, l1, l2)'' | Ship ''s'' can move between these locations. |
| ''outside(s)'' | The ship is outside the map. |
| ''entry(s, l)'' / ''exit(s, l)'' | Entry and exit points for the ship. |
| ''dead'' | A fatal collision has occurred. |
==== Main Actions ====
* ''move'': move the AUV between connected free locations.
* ''sample'': collect a resource at the current AUV location.
* ''enter-ship-free'': move a ship into a free location.
* ''enter-ship-auv'': move a ship into a location occupied by an AUV, destroying it.
* ''leave-ship'': remove the ship from a location.
* ''move-ship-free'': move the ship into a free location.
* ''move-ship-auv'': move the ship into a location occupied by the AUV, destroying it.
==== Reference Goal ====
auv_problem.add_goal(
And(
auv_fluents["operational"](auv1),
auv_fluents["sampled"](r1),
auv_fluents["sampled"](r2),
)
)
===== Running the Models =====
The notebooks use shared run parameters:
engine = None
write_pddl = True
With ''engine = None'', Unified Planning automatically selects any compatible available backend. This is the safest default in environments where a specific engine may not be installed.
To run the two problems:
print("\n=== PERESTROIKA ===")
if write_pddl:
export_pddl(perestroika_problem, DOMAINS_DIR / "perestroika")
print(solve_problem(perestroika_problem, engine=engine))
print("\n=== AUV ===")
if write_pddl:
export_pddl(auv_problem, DOMAINS_DIR / "auv")
print(solve_problem(auv_problem, engine=engine))
===== Files =====
* Notebooks: {{ :courses:pui:tutorials:pan_lab_student.zip | unsolved}}, {{ :courses:pui:tutorials:pan_lab_teacher.zip | solved}}