====== 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}}