This lab introduces classical planning modeling directly in Python with Unified Planning. Students construct two planning problems, solve them from in-memory objects, and optionally export the same models to PDDL.
Problem, Fluent, InstantaneousAction, objects, initial facts, and goals.
domain.pddl and problem.pddl.
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"))
The student notebook uses a gradual modeling workflow. For each domain, students:
The first action in each domain is already implemented as a worked example. The remaining actions and missing facts are marked with TODO comments.
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.
| 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. |
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.
perestroika_problem.add_goal( And( perestroika_fluents["alive"](), perestroika_fluents["taken"](c1), perestroika_fluents["taken"](c2), ) )
The AUV moves through underwater locations, collects samples, and interacts with a ship that can occupy locations and cause destructive collisions if modeled incorrectly.
| 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® | 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. |
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.
auv_problem.add_goal( And( auv_fluents["operational"](auv1), auv_fluents["sampled"](r1), auv_fluents["sampled"](r2), ) )
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))