Planners-11: Unified Planning

Navigation : Index | << LLM Planning | LOOP >>

Interface Unifiee pour Multi-Solveurs

Ce notebook presente unified-planning, une bibliotheque Python qui offre une interface unique pour interagir avec de multiples planificateurs (Fast Downward, Pyperplan, Tamer, etc.). Cette abstraction permet de comparer facilement différents solveurs et de changer de planificateur sans reecrire le code.

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez :

  1. Utiliser l’API unified-planning pour définir des problemes de planification
  2. Comparer les performances de différents solveurs sur un même problème
  3. Exploiter les fonctionnalites avancees : planification temporelle, fluents numériques
  4. Convertir entre le format Python et PDDL standard
  5. Choisir le solveur adapte selon le type de problème
  6. Integrer unified-planning dans un pipeline de planification

Prerequis

  • Avoir suivi Planners-2-PDDL-Basics
  • Connaissance de Python oriente objet
  • Comprendre le modèle STRIPS (predicats, actions, preconditions, effets)

Duree estimee : 40 minutes


1. Introduction a unified-planning

unified-planning est un projet de l’Union Europeenne (AIPlan4EU) qui vise a democratise l’acces aux outils de planification. Elle offre :

  • Une API Python intuitive pour définir domaines et problemes
  • Une interface unifiee pour 10+ planificateurs
  • Support pour PDDL, planification temporelle, et contraintes numériques
  • Comparaison automatique des performances

1.1 Planificateurs supportes

Planificateur Type Optimalite Specialites
Fast Downward Classique Optimal/Satisficing Heuristiques LM-cut, FF
Pyperplan Classique Satisficing Leger, rapide
Tamer Classique Satisficing Planification avec préférences
ENHSP Numérique Optimal Fluents numériques, temps
UP-SMT SMT Optimal Contraintes complexes
LPG Temporel Satisficing PDDL 2.1, durees
OSP Oversubscription Partiel Objectifs multiples

1.2 Installation et configuration

La bibliotheque s’installe avec pip. Chaque planificateur a son propre package optionnel.

# Installation des packages necessaires
# Decommentez les lignes si necessaire

# !pip install unified-planning
# !pip install up-fast-downward  # Fast Downward
# !pip install up-pyperplan      # Pyperplan
# !pip install up-tamer          # Tamer

# Imports principaux
import sys
import time
import warnings
from typing import List, Dict, Optional, Any

from unified_planning.shortcuts import *
# La banniere "Credits" affichee au premier appel d'un solveur unified_planning
# contient le chemin absolu de la source de la bibliotheque (fuite de chemin
# machine, #9997/#10000). On la supprime globalement une fois pour toutes,
# comme le suggere la banniere elle-meme (cf. aussi la cellule d'optimisation).
get_environment().credits_stream = None
# unified_planning emet un UserWarning "We cannot establish whether ... can solve"
# dont le traceback inclut le chemin absolu de la bibliotheque (meme fuite, #9997).
# Ce warning est purement informatif : le statut de resolution (ex. INTERNAL_ERROR
# pour les fluents numeriques) est deja gere explicitement dans les cellules.
warnings.filterwarnings("ignore", message=r"We cannot establish whether")
# Note: En unified-planning 1.3.0+, toutes les classes sont importees via shortcuts
# UserType, BoolType, IntType viennent de shortcuts, pas de model.types
from unified_planning.engines import Engine
from unified_planning.model import Problem, Action, Fluent, Object

# Imports pour les resultats
from unified_planning.engines.results import PlanGenerationResultStatus

print("unified-planning version:", __import__('unified_planning').__version__)
unified-planning version: 1.3.0

Verification des moteurs de planification disponibles sur le système et affichage de leurs capacites (planification classique, temporelle, numérique, etc.).

# Verification des moteurs disponibles
def check_available_engines():
    """Liste les planificateurs disponibles sur le systeme."""
    from unified_planning.environment import get_environment
    from unified_planning.engines import Factory
    
    # unified-planning 1.3.0+ requiere un environnement
    env = get_environment()
    factory = Factory(env)
    
    # factory.engines retourne une liste de noms de moteurs
    engines_list = factory.engines
    
    print("Planificateurs disponibles:")
    print("=" * 50)
    
    for name in sorted(engines_list):
        # Verifier si c'est un planificateur
        if 'oneshot' in name.lower() or 'planner' in name.lower() or name in ['pyperplan', 'fast-downward', 'tamer']:
            print(f"  - {name}")
    
    return engines_list

available = check_available_engines()
print(f"\nTotal: {len(available)} moteurs detectes")
Planificateurs disponibles:
==================================================
  - fast-downward
  - pyperplan
  - replanner[fast-downward-opt]
  - replanner[fast-downward]
  - replanner[pyperplan-opt]
  - replanner[pyperplan]

Total: 25 moteurs detectes

2. Definition de Problemes avec l’API

L’API unified-planning permet de définir des problemes directement en Python, sans ecrire de fichiers PDDL. C’est souvent plus intuitif et permet l’integration avec d’autres outils Python.

2.1 Types et Fluents

Les types utilisateur (UserType) representent les objets du domaine. Les fluents sont des fonctions qui retournent une valeur selon l’etat du monde.

# Creation d'un probleme de navigation robot
# Le robot doit se deplacer d'un point A a un point B

# Definition des types
Location = UserType('Location')
Robot = UserType('Robot')

# Definition des fluents (predicats variables)
# robot_at(r, l) = le robot r est a la location l
robot_at = Fluent('robot_at', BoolType(), r=Robot, l=Location)

# connected(l1, l2) = la location l1 est connectee a l2 (graphe statique)
connected = Fluent('connected', BoolType(), l1=Location, l2=Location)

# visited(l) = la location l a ete visitee
visited = Fluent('visited', BoolType(), l=Location)

print("Types et Fluents definis:")
print(f"  Types: Location, Robot")
print(f"  Fluents: robot_at, connected, visited")
Types et Fluents definis:
  Types: Location, Robot
  Fluents: robot_at, connected, visited

2.2 Actions et Effets

Une action a des paramètres, des preconditions et des effets. L’API utilise des méthodes fluentes pour ajouter ces éléments.

# Definition de l'action 'move'
# Le robot se deplace d'une location a une autre connectee

move = InstantaneousAction('move', r=Robot, l_from=Location, l_to=Location)
r = move.parameter('r')
l_from = move.parameter('l_from')
l_to = move.parameter('l_to')

# Preconditions
move.add_precondition(connected(l_from, l_to))  # Les lieux sont connectes
move.add_precondition(robot_at(r, l_from))      # Le robot est au depart

# Effets
move.add_effect(robot_at(r, l_from), False)     # Le robot quitte l_from
move.add_effect(robot_at(r, l_to), True)        # Le robot arrive a l_to
move.add_effect(visited(l_to), True)            # Marquer l_to comme visite

print("Action 'move' definie:")
print(f"  Parametres: r:Robot, l_from:Location, l_to:Location")
print(f"  Preconditions: connected(l_from, l_to), robot_at(r, l_from)")
print(f"  Effets: robot_at(r, l_from)=False, robot_at(r, l_to)=True, visited(l_to)=True")
Action 'move' definie:
  Parametres: r:Robot, l_from:Location, l_to:Location
  Preconditions: connected(l_from, l_to), robot_at(r, l_from)
  Effets: robot_at(r, l_from)=False, robot_at(r, l_to)=True, visited(l_to)=True

2.3 Problème complet

Un problème complet comprend : le domaine (types, fluents, actions) et l’instance (objets, etat initial, buts).

# Creation du probleme complet : navigation dans un labyrinthe
# Instance NON TRIVIALE : 15 locations en reseau avec impasses (dead-ends).
# Les dead-ends (L3, L6, L9, L12, L14) sont des branches sans issue : une recherche
# aveugle (BFS/DFS pur) y perd du temps, tandis qu'un solveur heuristique (h^FF,
# landmarks) les elimine vite. C'est ce diferencia que unified-planning met en valeur.
problem = Problem('robot_navigation')

# Ajout des fluents au probleme
problem.add_fluent(robot_at, default_initial_value=False)
problem.add_fluent(connected, default_initial_value=False)
problem.add_fluent(visited, default_initial_value=False)

# Ajout de l'action
problem.add_action(move)

# Creation des objets : 15 locations + 1 robot
locations = [Object(f'L{i}', Location) for i in range(15)]
robot = Object('robot1', Robot)

for loc in locations:
    problem.add_object(loc)
problem.add_object(robot)

# Graphe du labyrinthe (connexions orientees bidirectionnelles) :
#   Chemin principal (corridor) : L0 - L1 - L2 - L4 - L7 - L10 - L13
#   Branches secondaires menant au but : L10 - L11, L13 - L11 (le but L11 est atteignable)
#   Impasses (dead-ends) : L3 (depuis L1), L5 (depuis L4), L6 (depuis L2),
#                         L8 (depuis L7), L9 (depuis L2), L12 (depuis L10), L14 (depuis L13)
#   L'objectif : rejoindre L11 depuis L0. Chemin court : L0-L1-L2-L4-L7-L10-L11 (6 moves),
#   chemin long via L13 : L0-...-L10-L13-L11 (7 moves). 7 impasses penchant la recherche aveugle.
connections = [
    # Chemin principal L0 -> L1 -> L2 -> L4 -> L7 -> L10 -> L13
    (0, 1), (1, 0),
    (1, 2), (2, 1),
    (2, 4), (4, 2),
    (4, 7), (7, 4),
    (7, 10), (10, 7),
    (10, 13), (13, 10),
    # Acces au but L11 (depuis L10 ET L13 : deux routes vers le but)
    (10, 11), (11, 10),
    (13, 11), (11, 13),
    # Dead-ends (branches sans issue) : pieges pour la recherche aveugle
    (1, 3), (3, 1),      # impasse L3
    (4, 5), (5, 4),      # impasse L5
    (2, 6), (6, 2),      # impasse L6
    (7, 8), (8, 7),      # impasse L8
    (2, 9), (9, 2),      # impasse L9
    (10, 12), (12, 10),  # impasse L12
    (13, 14), (14, 13),  # impasse L14
]

for i, j in connections:
    problem.set_initial_value(connected(locations[i], locations[j]), True)

# Etat initial : robot a L0 (entree du labyrinthe)
problem.set_initial_value(robot_at(robot, locations[0]), True)
problem.set_initial_value(visited(locations[0]), True)

# But : atteindre L11 (sortie du labyrinthe)
problem.add_goal(robot_at(robot, locations[11]))

print(f"Probleme '{problem.name}' cree")
print(f"  Objets : {len(locations)} locations, 1 robot")
print(f"  Connexions : {len(connections)} aretes")
print(f"  Etat initial : robot a L0 (entree)")
print(f"  But : atteindre L11 (sortie)")
print(f"  7 impasses (dead-ends) : L3, L5, L6, L8, L9, L12, L14 -- pieges pour la recherche aveugle")
print(f"  Chemin optimal attendu : ~6 moves (L0-L1-L2-L4-L7-L10-L11, route courte)")
Probleme 'robot_navigation' cree
  Objets : 15 locations, 1 robot
  Connexions : 30 aretes
  Etat initial : robot a L0 (entree)
  But : atteindre L11 (sortie)
  7 impasses (dead-ends) : L3, L5, L6, L8, L9, L12, L14 -- pieges pour la recherche aveugle
  Chemin optimal attendu : ~6 moves (L0-L1-L2-L4-L7-L10-L11, route courte)

Interpretation : API unified-planning

Structure du modèle : Le problème suit le paradigme STRIPS avec une API Python orientee objet.

Élément API unified-planning Equivalent PDDL
Types UserType('Location') (:types location)
Fluents Fluent('robot_at', BoolType(), ...) (:predicates (robot_at ?r ?l))
Actions InstantaneousAction('move', ...) (:action move ...)
Preconditions action.add_precondition(...) :precondition (and ...)
Effets action.add_effect(...) :effect (and ...)

Points cles : 1. L’API est plus verbeuse que PDDL mais plus expressive en Python 2. Les paramètres d’action sont accessibles via action.parameter('name') 3. default_initial_value simplifie l’initialisation des fluents

Indice : Exercice robot navigation

Approche suggeree pour resoudre ce problème :

  1. Structure du code (suivre les commentaires dans la cellule exercise) :

    # 1. Types
    Location = UserType("Location")
    Robot = UserType("Robot")
    
    # 2. Fluents
    at = Fluent("at", BoolType(), robot=Robot, location=Location)
    free = Fluent("free", BoolType(), robot=Robot)
    holding = Fluent("holding", BoolType(), robot=Robot, box=Box)
    at_box = Fluent("at_box", BoolType(), box=Box, location=Location)
    
    # 3. Actions (deplacer, charger, decharger)
    move = InstantaneousAction("deplacer", robot=Robot, src=Location, dst=Location)
    move.add_precondition(at(move.robot, move.src))
    move.add_effect(at(move.robot, move.src), False)
    move.add_effect(at(move.robot, move.dst), True)
  2. Objets : Créer les 4 locations + robot + 2 boxes

  3. Etat initial : robot au depot, boxes en zone_a/zone_b

  4. But : les 2 boxes doivent etre au depot

Note : L’action charger doit verifier que le robot est au même endroit que la box ET que le robot a les bras libres (free(robot)).


3. Resolution et Comparaison de Solveurs

L’un des grands avantages d’unified-planning est la possibilite de tester facilement différents solveurs sur le même problème.

# Resolution avec un seul solveur (Pyperplan - leger et rapide)

def solve_with_planner(problem, planner_name='pyperplan', verbose=True):
    """Resout un probleme avec un planificateur donne."""
    start_time = time.time()
    
    try:
        with OneshotPlanner(name=planner_name) as planner:
            result = planner.solve(problem)
        
        solve_time = time.time() - start_time
        
        if verbose:
            print(f"\n{'='*50}")
            print(f"Solveur: {planner_name}")
            print(f"{'='*50}")
            print(f"Statut: {result.status.name}")
            print(f"Temps: {solve_time:.3f}s")
            
            if result.plan is not None:
                print(f"\nPlan trouve ({len(result.plan.actions)} actions):")
                for i, action in enumerate(result.plan.actions):
                    print(f"  {i+1}. {action}")
            else:
                print("Aucun plan trouve")
        
        return {
            'planner': planner_name,
            'status': result.status.name,
            'time': solve_time,
            'plan': result.plan,
            'plan_length': len(result.plan.actions) if result.plan else None
        }
    
    except Exception as e:
        if verbose:
            print(f"Erreur avec {planner_name}: {e}")
        return {
            'planner': planner_name,
            'status': 'ERROR',
            'time': time.time() - start_time,
            'plan': None,
            'plan_length': None,
            'error': str(e)
        }

# Test avec Pyperplan
result_pyperplan = solve_with_planner(problem, 'pyperplan')

==================================================
Solveur: pyperplan
==================================================
Statut: SOLVED_SATISFICING
Temps: 0.024s

Plan trouve (6 actions):
  1. move(robot1, L0, L1)
  2. move(robot1, L1, L2)
  3. move(robot1, L2, L4)
  4. move(robot1, L4, L7)
  5. move(robot1, L7, L10)
  6. move(robot1, L10, L11)

Interpretation : Resolution avec un seul solveur

Sortie obtenue : pyperplan resout le labyrinthe et renvoie un statut SOLVED_SATISFICING avec un plan de 6 actions menant le robot de L0 (entree) a L11 (sortie).

Le plan trouve suit le chemin court traverse le corridor principal L0 -> L1 -> L2 -> L4 -> L7 -> L10, puis atteint L11. Les 7 impasses (L3, L5, L6, L8, L9, L12, L14) ne sont jamais visitees : l’heuristique h^FF interne a pyperplan les elimine des le depart, car elles eloignent du but au lieu d’en rapprocher. C’est precisement ce qu’un moteur de planification apporte de plus qu’une recherche aveugle (BFS/DFS), qui devrait explorer ces branches sans issue avant de les ecarter.

Workflow de resolution : 1. Creer le solveur (OneshotPlanner) 2. Appeler solve(problem) 3. Analyser le statut de retour 4. Extraire le plan si succes

Statuts possibles : - SOLVED_SATISFICING : Plan trouve (pas forcement optimal) - SOLVED_OPTIMALLY : Plan optimal trouve - UNSATISFIABLE : Probleme insoluble - TIMEOUT : Timeout atteint - ERROR : Erreur technique (solveur absent, probleme mal defini)

Note technique : La classe OneshotPlanner est un contexte manager qui gere les ressources du solveur. Il est important de l’utiliser dans un bloc with pour assurer la liberation des ressources. unified_planning affiche normalement une banniere de credits au premier appel d’un solveur (provenance du moteur, equipe de developpement) ; nous la supprimons globalement dans la cellule d’import, car elle contient le chemin d’installation de la bibliotheque (fuite de chemin machine). Reste que le statut SOLVED_SATISFICING et le plan produit sont la preuve que unified-planning delegue bien a un moteur externe (pyperplan, Universite de Fribourg) plutot que d’emuler la planification en Python.

3.1 Comparaison multi-solveurs

Comparons les performances de plusieurs planificateurs sur le même problème.

def compare_solvers(problem, solvers=['pyperplan', 'fast-downward'], timeout=30):
    """Compare plusieurs solveurs sur un meme probleme."""
    results = []
    
    print("Comparaison de solveurs")
    print("=" * 60)
    print(f"Probleme: {problem.name}")
    print(f"Timeout: {timeout}s par solveur")
    print("=" * 60)
    
    for solver in solvers:
        print(f"\nTest de {solver}...")
        result = solve_with_planner(problem, solver, verbose=False)
        result['timeout'] = timeout
        results.append(result)
        
        # Affichage compact
        status = result['status']
        time_str = f"{result['time']:.3f}s"
        plan_len = result['plan_length'] if result['plan_length'] else 'N/A'
        
        print(f"  Statut: {status}, Temps: {time_str}, Plan: {plan_len} actions")
    
    return results

# Liste des solveurs a tester (ajuster selon ce qui est installe)
solvers_to_test = ['pyperplan', 'fast-downward']

# Lancer la comparaison
comparison_results = compare_solvers(problem, solvers_to_test)
Comparaison de solveurs
============================================================
Probleme: robot_navigation
Timeout: 30s par solveur
============================================================

Test de pyperplan...
  Statut: SOLVED_SATISFICING, Temps: 0.008s, Plan: 6 actions

Test de fast-downward...
  Statut: SOLVED_SATISFICING, Temps: 0.294s, Plan: 6 actions

Interpretation : Comparaison multi-solveurs

Sortie obtenue : Les deux solveurs testes resolvent le labyrinthe, avec des temps differents mais un plan de meme longueur (6 actions).

Solveur Statut Plan
pyperplan SOLVED_SATISFICING 6 actions
fast-downward SOLVED_SATISFICING 6 actions

Pourquoi pyperplan est plus rapide ici : sur un labyrinthe de 15 noeuds, l’espace de recherche est petit, donc le temps est domine par le demarrage du moteur (startup overhead), pas par la recherche elle-meme. pyperplan est un planner STRIPS leger ecrit en Python pur : il se lance presque instantanement. fast-downward est un moteur C++ externe, beaucoup plus puissant (heuristiques configurables : LM-cut, FF, merge-and-shrink) mais avec une phase d’initialisation plus lourde. Le constat “pyperplan plus rapide” reflete donc la legerete du moteur sur un petit probleme, pas une inferiorite de fast-downward – dont les heuristiques prennent tout leur sens sur des problemes plus gros (voir ci-dessous).

Workflow de comparaison :

  1. Initialisation : Creer le probleme une seule fois
  2. Tests sequentiels : Lancer chaque solveur avec un timeout
  3. Collecte de resultats : Statut, temps, longueur du plan
  4. Analyse : Identifier le plus rapide, le plan le plus court

Note technique : unified-planning permet de changer de solveur en modifiant une seule ligne (name='fast-downward' au lieu de name='pyperplan'), ce qui facilite enormement le benchmarking. Les deux plans font 6 actions car les deux moteurs trouvent le chemin optimal court (L0-…-L10-L11) ; sur un probleme plus riche ou un solveur serait satisficing et l’autre optimal, les longueurs de plan pourraient diverger.

Les temps exacts sont ceux imprimes par la cellule de comparaison ci-dessus. Ils dependent de la machine et de son demarrage a froid : les recopier ici les figerait sur une execution alors que la cellule les reimprime a chaque passage — c’est pourquoi la colonne Temps n’est pas reproduite dans ce tableau. Ce qui reste stable d’une execution a l’autre, et qui porte la lecture, c’est l’ordre des solveurs et la longueur des plans.

Affichage formate sous forme de tableau des résultats de la comparaison entre solveurs, incluant le statut, le temps de resolution et la longueur du plan produit.

# Affichage formate de la comparaison
def display_comparison_table(results):
    """Affiche un tableau de comparaison des resultats."""
    print("\n" + "=" * 80)
    print("TABLEAU COMPARATIF")
    print("=" * 80)
    print(f"{'Solveur':<20} | {'Statut':<15} | {'Temps':<10} | {'Longueur Plan':<15}")
    print("-" * 80)
    
    for r in results:
        planner = r['planner']
        status = r['status']
        time_str = f"{r['time']:.3f}s"
        plan_len = str(r['plan_length']) if r['plan_length'] else 'N/A'
        
        print(f"{planner:<20} | {status:<15} | {time_str:<10} | {plan_len:<15}")
    
    print("=" * 80)
    
    # Analyse des resultats
    valid_results = [r for r in results if r['status'] == 'SOLVED_SATISFICING']
    
    if valid_results:
        fastest = min(valid_results, key=lambda x: x['time'])
        shortest = min(valid_results, key=lambda x: x['plan_length'] or float('inf'))
        
        print(f"\nPlus rapide: {fastest['planner']} ({fastest['time']:.3f}s)")
        print(f"Plan le plus court: {shortest['planner']} ({shortest['plan_length']} actions)")

display_comparison_table(comparison_results)

================================================================================
TABLEAU COMPARATIF
================================================================================
Solveur              | Statut          | Temps      | Longueur Plan  
--------------------------------------------------------------------------------
pyperplan            | SOLVED_SATISFICING | 0.008s     | 6              
fast-downward        | SOLVED_SATISFICING | 0.294s     | 6              
================================================================================

Plus rapide: pyperplan (0.008s)
Plan le plus court: pyperplan (6 actions)

Interpretation : Comparaison des solveurs

Observations typiques :

Solveur Caractéristiques Quand l’utiliser
Pyperplan Rapide, leger, satisficing Prototypage, tests rapides
Fast Downward Optimal ou satisficing, heuristiques puissantes Production, gros problemes
Tamer Préférences, qualite de plan Plans avec priorites
ENHSP Numérique, temporel Problemes avec couts/temps

Points cles : 1. Le même problème peut donner des plans différents selon le solveur 2. Les solveurs optimaux (A*) sont plus lents mais garantissent l’optimalite 3. Les solveurs satisficing (GBFS, EHC) sont plus rapides mais peuvent donner des plans sous-optimaux

Lecture des colonnes du tableau (celui qu’imprime la cellule de comparaison ci-dessus) :

  • Temps : latence de resolution, mesure de machine — dominee sur cette instance par le demarrage du moteur, pas par la recherche. C’est pourquoi elle n’est pas recopiee dans les cellules d’interpretation.
  • Statut : SOLVED_OPTIMALLY (plan optimal prouve), SOLVED_SATISFICING (plan trouve, optimalite non garantie), UNSATISFIABLE (insolubilite prouvee).
  • Longueur Plan : nombre d’actions du plan, plus court etant mieux. Les deux moteurs convergent ici vers le chemin optimal de 6 moves.

Interpretation : Qui gagne, et quand ?

Sortie obtenue : Sur ce labyrinthe de 15 noeuds, pyperplan est le plus rapide — l’ecart vient du demarrage du moteur, que la cellule de comparaison imprime a chaque execution — mais les deux solveurs produisent le meme plan optimal de 6 actions. Il n’y a pas de “vainqueur” absolu : chaque moteur a son domaine de predilection.

Comparaison theorique des solveurs unified-planning :

Solveur Optimalite Vitesse Specialites
Pyperplan Satisficing Tres rapide (start leger) Problemes simples, prototypage
Fast Downward Optimal/Satisficing Moyenne (start lourd) Heuristiques puissantes (LM-cut, FF, merge-and-shrink)
Tamer Satisficing Rapide Preferences, plans avec qualite
ENHSP Optimal Lent Fluents numeriques, PDDL 2.1
LPG Satisficing Moyenne Planification temporelle

Quand utiliser chaque solveur : - Pyperplan : Tests rapides, petits problemes (<100 objets) – comme ici, ou sa legerete l’emporte. - Fast Downward : Production, problemes complexes – ses heuristiques dominent des que l’espace de recherche devient grand (le startup fixe devient negligeable face au cout de recherche). - ENHSP : Problemes numeriques (couts, ressources). - LPG : Planification temporelle (PDDL 2.1).

Lecon de cette instance : le labyrinthe a 15 noeuds et 7 impasses, ce qui est assez riche pour qu’aucun des deux moteurs ne degeneres (tous deux trouvent un plan non trivial de 6 actions en evitant les dead-ends), mais assez petit pour que le temps soit domine par le startup. Pour voir fast-downward prendre l’avantage, il faudrait un graphe beaucoup plus large ou pyperplan (satisficing, heuristique limitee) ralentirait alors que fast-downward (heuristiques configurables) resterait efficace.

Note importante : Le choix du solveur depend fortement du type de probleme ET de sa taille. Un solveur excellent sur Block World peut etre mediocre sur Logistics. C’est tout l’interet d’unified-planning : tester facilement plusieurs solveurs et laisser les metriques (temps, longueur de plan) decider.


4. Fonctionnalites Avancees

unified-planning supporte des extensions au modèle STRIPS classique : planification temporelle et fluents numériques.

4.1 Actions duratives (temporelles)

Les DurativeActions ont une duree et des effets conditionnes par le temps (au debut, a la fin, pendant).

# Exemple de planification temporelle : livraison avec temps de trajet

# Nouveaux types pour le probleme temporel
City = UserType('City')
Truck = UserType('Truck')
Package = UserType('Package')

# Fluents
truck_at = Fluent('truck_at', BoolType(), t=Truck, c=City)
package_at = Fluent('package_at', BoolType(), p=Package, c=City)
package_in = Fluent('package_in', BoolType(), p=Package, t=Truck)
distance = Fluent('distance', IntType(), c1=City, c2=City)  # Fluent numerique

# Action durative : conduire
drive = DurativeAction('drive', t=Truck, c1=City, c2=City)
t = drive.parameter('t')
c1 = drive.parameter('c1')
c2 = drive.parameter('c2')

# Duree : depend de la distance (simplifie a fixe pour l'exemple)
drive.set_fixed_duration(10)  # 10 unites de temps

# Preconditions au debut (API unified-planning 1.3.0+)
# Utiliser add_condition avec StartTiming() pour les conditions au debut
from unified_planning.shortcuts import StartTiming
drive.add_condition(StartTiming(), truck_at(t, c1))
drive.add_condition(StartTiming(), Not(Equals(c1, c2)))

# Effets: utiliser add_effect avec timing
from unified_planning.shortcuts import EndTiming
drive.add_effect(StartTiming(), truck_at(t, c1), False)    # Quitte c1 au debut
drive.add_effect(EndTiming(), truck_at(t, c2), True)       # Arrive a c2 a la fin

print("Action durative 'drive' definie:")
print(f"  Duree fixe: 10 unites de temps")
print(f"  Preconditions: truck_at(t, c1), c1 != c2")
print(f"  Effets: truck_at(t, c1)=False au debut, truck_at(t, c2)=True a la fin")
Action durative 'drive' definie:
  Duree fixe: 10 unites de temps
  Preconditions: truck_at(t, c1), c1 != c2
  Effets: truck_at(t, c1)=False au debut, truck_at(t, c2)=True a la fin

4.2 Fluents numériques et couts

Les fluents peuvent etre de type entier ou flottant, permettant de modeliser des ressources (carburant, argent) et des couts.

# Exemple avec fluents numeriques : robot avec batterie

# Types
Loc = UserType('Loc')

# Fluents booleens
at = Fluent('at', BoolType(), l=Loc)
adjacent = Fluent('adjacent', BoolType(), l1=Loc, l2=Loc)

# Fluents numeriques
battery = Fluent('battery', IntType())  # Niveau de batterie global

# Action avec cout numerique
move_numeric = InstantaneousAction('move', l_from=Loc, l_to=Loc)
l_from = move_numeric.parameter('l_from')
l_to = move_numeric.parameter('l_to')

# Preconditions
move_numeric.add_precondition(at(l_from))
move_numeric.add_precondition(adjacent(l_from, l_to))
move_numeric.add_precondition(GE(battery(), 10))  # Batterie >= 10

# Effets
move_numeric.add_effect(at(l_from), False)
move_numeric.add_effect(at(l_to), True)
move_numeric.add_decrease_effect(battery(), 10)  # Consomme 10 unites

print("Action 'move' avec contrainte de batterie:")
print(f"  Preconditions: at(l_from), adjacent(l_from, l_to), battery >= 10")
print(f"  Effets: at(l_from)=False, at(l_to)=True, battery -= 10")
Action 'move' avec contrainte de batterie:
  Preconditions: at(l_from), adjacent(l_from, l_to), battery >= 10
  Effets: at(l_from)=False, at(l_to)=True, battery -= 10

Creation du problème numérique avec le robot et sa batterie : initialisation des fluents, definition de l’etat initial et specification du but a atteindre.

# Creation du probleme numerique
numeric_problem = Problem('robot_battery')

# Ajout des fluents
numeric_problem.add_fluent(at, default_initial_value=False)
numeric_problem.add_fluent(adjacent, default_initial_value=False)
numeric_problem.add_fluent(battery, default_initial_value=0)  # Valeur par defaut

numeric_problem.add_action(move_numeric)

# Objets
loc_a = Object('A', Loc)
loc_b = Object('B', Loc)
loc_c = Object('C', Loc)

numeric_problem.add_objects([loc_a, loc_b, loc_c])

# Etat initial
numeric_problem.set_initial_value(at(loc_a), True)
numeric_problem.set_initial_value(battery(), 50)  # 50 unites de batterie
numeric_problem.set_initial_value(adjacent(loc_a, loc_b), True)
numeric_problem.set_initial_value(adjacent(loc_b, loc_c), True)

# But
numeric_problem.add_goal(at(loc_c))

print("Probleme 'robot_battery' cree")
print(f"  Batterie initiale: 50")
print(f"  But: atteindre C (2 mouvements, 20 unites de batterie)")

# Note: Pyperplan ne supporte pas les fluents numeriques
# Utiliser ENHSP ou un autre solveur numerique
Probleme 'robot_battery' cree
  Batterie initiale: 50
  But: atteindre C (2 mouvements, 20 unites de batterie)

Exemple guide 1 : Resolution complete du problème robot_battery

Le problème robot_battery est maintenant défini avec un robot qui doit aller de A a C en passant par B, avec une contrainte de batterie (50 unites, 10 par mouvement). Verifions que le problème est coherent et tentons de le résoudre avec Fast Downward (moteur classique qui ne supporte pas les fluents numériques — le plan affiché est le résultat attendu, calculé à la main).

# Exemple guide 1 : Resolution complete avec affichage du plan
from unified_planning.io import PDDLWriter

# Verification : afficher le domaine PDDL genere
writer_battery = PDDLWriter(numeric_problem)
print("Domaine PDDL genere :")
print(writer_battery.get_domain()[:400])
print("...")

# Resolution avec Fast Downward
print("\nResolution avec fast-downward...")
try:
    with OneshotPlanner(name='fast-downward') as planner:
        result = planner.solve(numeric_problem)
    
    if result.status in (PlanGenerationResultStatus.SOLVED_SATISFICING,
                          PlanGenerationResultStatus.SOLVED_OPTIMALLY):
        print(f"Statut : {result.status.name}")
        print(f"Plan trouve ({len(result.plan.actions)} actions) :")
        print("=" * 50)
        for i, action in enumerate(result.plan.actions):
            params = ', '.join(str(p) for p in action.actual_parameters)
            print(f"  {i+1}. {action.action.name}({params})")
        print("=" * 50)
        print(f"Batterie restante : {50 - 10 * len(result.plan.actions)} unites")
    else:
        # Solveur ne supporte pas les fluents numeriques ou erreur interne
        print(f"Statut : {result.status.name}")
        print(f"\nPlan attendu (2 actions) :")
        print("=" * 50)
        print("  1. move(A, B)    # batterie: 50 -> 40")
        print("  2. move(B, C)    # batterie: 40 -> 30")
        print("=" * 50)
        print(f"Batterie restante : 30 unites")
except Exception as e:
    # Si le solveur n'est pas disponible
    print(f"Solveur non disponible ({e})")
    print(f"\nPlan attendu (2 actions) :")
    print("=" * 50)
    print("  1. move(A, B)    # batterie: 50 -> 40")
    print("  2. move(B, C)    # batterie: 40 -> 30")
    print("=" * 50)
    print(f"Batterie restante : 30 unites")
Domaine PDDL genere :
(define (domain robot_battery-domain)
 (:requirements :strips :typing :numeric-fluents)
 (:types loc)
 (:predicates 
             (at ?l - loc)
             (adjacent ?l1 - loc ?l2 - loc)
 )
 (:functions 
             (battery)
 )
 (:action move
  :parameters ( ?l_from - loc ?l_to - loc)
  :precondition (and (at ?l_from) (adjacent ?l_from ?l_to) (<= 10 (battery)))
  :effect (and (not (at ?l_from))
...

Resolution avec fast-downward...
Statut : INTERNAL_ERROR

Plan attendu (2 actions) :
==================================================
  1. move(A, B)    # batterie: 50 -> 40
  2. move(B, C)    # batterie: 40 -> 30
==================================================
Batterie restante : 30 unites

Interpretation : Exemple guide 1

Cycle complet : definition du problème (fluents booléens + numériques) → tentative avec Fast Downward (échoue : INTERNAL_ERROR, fluents numériques non supportés par le moteur classique) → plan attendu calculé à la main.

Points a retenir : 1. Les fluents numériques (IntType) permettent de modeliser des ressources consommables (batterie, carburant, budget) 2. La precondition GE(battery(), 10) empeche toute action si la ressource est insuffisante 3. add_decrease_effect modifie le fluent numérique a chaque action — un solveur numérique (ex. ENHSP) en tiendrait compte dans sa recherche (Fast Downward, moteur classique, ne gère pas les fluents numériques) 4. Le plan attendu minimise les mouvements (2 actions pour A→B→C), consommant 20/50 unites

Interpretation : Fonctionnalites avancees

Extensions supportees :

Feature API Solveurs compatibles
Duree fixe set_fixed_duration(n) LPG, ENHSP, Temporal planner
Duree variable set_duration_constraint(...) Planificateurs temporels
Effets temporises Start(...), End(...) LPG, AOX
Fluents numériques IntType(), FloatType() ENHSP, SMT planners
Augmentation add_increase_effect(...) Solveurs numériques
Diminution add_decrease_effect(...) Solveurs numériques

Points cles : 1. Les actions duratives permettent le parallelisme (plusieurs actions en même temps) 2. Les fluents numériques ajoutent un niveau de complexite (solveurs spécifiques necessaires) 3. Toujours verifier que le solveur supporte les fonctionnalitees utilisees

Exercice 3 : Ajouter une action de recharge au robot

Le problème robot_battery donne 50 unites de batterie pour 2 mouvements (cout 20). Etendez ce problème pour permettre au robot de recharger sa batterie.

Objectif : Ajoutez une action recharge qui remonte le niveau de batterie a 50, puis modifiez le problème pour que le robot doive atteindre un objectif plus eloigne necessitant 3 mouvements (batterie insuffisante sans recharge).

Indices : - L’action recharge(l) doit avoir comme precondition at(l) et que la location l soit une station de recharge (nouveau fluent charging_station(l)) - Utilisez add_increase_effect pour augmenter la batterie - Ajoutez une 4eme location loc_d connectee a loc_c et mettez le but a loc_d - Avec 50 de batterie et 4 mouvements (40 unites), le robot doit passer par une station de recharge

# Exercice 3 : Ajouter une action de recharge au robot
# TODO etudiant : etendez le probleme robot_battery avec recharge
# Etape 1 : creez un nouveau fluent charging_station(l) et une action recharge(l)
# Etape 2 : ajoutez loc_d connectee a loc_c, but = atteindre loc_d (3 mouvements)
# Etape 3 : placez une station de recharge en loc_b
# Etape 4 : resolvez avec un solveur numerique (ENHSP ou fast-downward)
# Indice : recharge add_increase_effect(battery(), 50 - battery()) pour plein

# Reutiliser les types et fluents definis ci-dessus (Loc, at, adjacent, battery)
# charging_station = Fluent('charging_station', BoolType(), l=Loc)
# recharge = InstantaneousAction('recharge', l=Loc)
# l = recharge.parameter('l')
# recharge.add_precondition(at(l))
# recharge.add_precondition(charging_station(l))
# recharge.add_increase_effect(battery(), 50)  # ou set a 50

print("Exercice a completer : ajouter une action de recharge au robot")
Exercice a completer : ajouter une action de recharge au robot

Exemple guide 2 : Optimisation – minimiser le cout total d’un plan

Jusqu’ici, les planificateurs ont resolu des problemes de satisfaction : trouver UN plan realisable (une sequence d’actions menant au but), comme le robot qui atteint sa destination avec assez de batterie. C’est l’equivalent planification de Solver().check() == sat en SMT.

La capacite distinctive d’un planificateur d’optimisation est la quality metric : parmi tous les plans realisables, trouver celui qui minimise un cout (ou maximise une recompense). C’est le coeur metier de la planification logistique (tournees de livraison minimisant le kilometrage), routing (reseaux), ordonnancement (makespan minimal). unified-planning l’exprime via problem.add_quality_metric(MinimizeActionCosts(...)), et un moteur optimal (ex. fast-downward-opt) certifie qu’aucun plan moins cher n’existe – la ou un solveur satisficing (fast-downward) se contente du premier plan trouve, sans garantie de cout.

Demonstration : une livraison de A vers D sur un graphe a couts heterogenes. Le saut direct A -> D coute cher (10) ; le detour A -> B -> C -> D coute 3 fois moins (1 + 1 + 1 = 3). Le plan le plus court (1 action) n’est pas le moins cher – c’est precisement ce qui rend l’optimisation discriminante (un cas uniforme la rendrait triviale).

"""Notebook cell source for Planners-11 optimization demo (exec=20).

Self-contained: re-imports unified_planning.shortcuts (idempotent), suppresses
credits, uses distinctive names (opt_*) to avoid namespace collision with the
robot_battery example earlier in the notebook.
"""
from unified_planning.shortcuts import *
get_environment().credits_stream = None  # silencer le banner Fast Downward

# Probleme de livraison : aller de A a D sur un graphe a couts heterogenes.
# Le saut direct A->D est CHER (10) ; le detour A->B->C->D est BON MARCHE (1+1+1=3).
# => Le plan le plus court (1 action) n'est PAS le moins cher : l'optimisation compte.
Loc = UserType("Loc")
at = Fluent("at", BoolType(), l=Loc)
adjacent = Fluent("adjacent", BoolType(), l1=Loc, l2=Loc)
edge_cost = Fluent("edge_cost", IntType(), l1=Loc, l2=Loc)

opt_move = InstantaneousAction("move", l_from=Loc, l_to=Loc)
lf = opt_move.parameter("l_from")
lt = opt_move.parameter("l_to")
opt_move.add_precondition(at(lf))
opt_move.add_precondition(adjacent(lf, lt))
opt_move.add_effect(at(lf), False)
opt_move.add_effect(at(lt), True)

opt_problem = Problem("delivery_min_cost")
opt_problem.add_fluent(at, default_initial_value=False)
opt_problem.add_fluent(adjacent, default_initial_value=False)
opt_problem.add_fluent(edge_cost, default_initial_value=0)
opt_problem.add_action(opt_move)

a, b, c, d = Object("A", Loc), Object("B", Loc), Object("C", Loc), Object("D", Loc)
opt_problem.add_objects([a, b, c, d])
opt_problem.set_initial_value(at(a), True)
for (x, y) in [(a, b), (b, c), (c, d), (a, d)]:
    opt_problem.set_initial_value(adjacent(x, y), True)
    opt_problem.set_initial_value(adjacent(y, x), True)
opt_problem.set_initial_value(edge_cost(a, b), 1)
opt_problem.set_initial_value(edge_cost(b, c), 1)
opt_problem.set_initial_value(edge_cost(c, d), 1)
opt_problem.set_initial_value(edge_cost(a, d), 10)
opt_problem.add_goal(at(d))

# === CAPACITE SIGNATURE : quality metric d'optimisation (MinimizeActionCosts) ===
# Avant, on cherchait un plan "qui marche" (satisfaction). Ici on minimise le
# cout total -- c'est ce qui distingue un planificateur d'optimisation (logistique,
# routage, ordonnancement) d'un simple trouveur de plan.
opt_problem.add_quality_metric(MinimizeActionCosts(costs={opt_move: edge_cost(lf, lt)}))


def plan_cost(plan):
    total = 0
    for act in plan.actions:
        ap = act.actual_parameters
        total += opt_problem.initial_value(edge_cost(ap[0], ap[1])).constant_value()
    return total


print("Probleme : livraison A -> D, couts heterogenes par trajet.")
print("  Direct A->D       : 1 action, cout 10")
print("  Detour A->B->C->D : 3 actions, cout 1+1+1 = 3")
print("  => plan le plus court != plan le moins cher.\n")

print("=== fast-downward-opt (OPTIMAL : minimise le cout total) ===")
with OneshotPlanner(name="fast-downward-opt") as planner:
    res = planner.solve(opt_problem)
    opt_plan = res.plan
    print(f"  Plan optimal    : {opt_plan}")
    print(f"  Cout minimal    : {plan_cost(opt_plan)}")
    print(f"  Nombre d'actions: {len(list(opt_plan.actions))}\n")

print("=== fast-downward (SATISFICING : un plan, sans garantie de cout) ===")
with OneshotPlanner(name="fast-downward") as planner:
    res2 = planner.solve(opt_problem)
    sat_plan = res2.plan
    print(f"  Plan satisficing : {sat_plan}")
    print(f"  Cout obtenu      : {plan_cost(sat_plan)}")
    print(f"  Nombre d'actions : {len(list(sat_plan.actions))}\n")

gap = plan_cost(sat_plan) - plan_cost(opt_plan)
print(f">>> L'optimal (cout {plan_cost(opt_plan)}) bat le satisficing "
      f"(cout {plan_cost(sat_plan)}) : l'optimization trouve un plan "
      f"{gap} unites moins cher -- avec PLUS d'actions.")
Probleme : livraison A -> D, couts heterogenes par trajet.
  Direct A->D       : 1 action, cout 10
  Detour A->B->C->D : 3 actions, cout 1+1+1 = 3
  => plan le plus court != plan le moins cher.

=== fast-downward-opt (OPTIMAL : minimise le cout total) ===
  Plan optimal    : SequentialPlan:
    move(A, B)
    move(B, C)
    move(C, D)
  Cout minimal    : 3
  Nombre d'actions: 3

=== fast-downward (SATISFICING : un plan, sans garantie de cout) ===
  Plan satisficing : SequentialPlan:
    move(A, D)
  Cout obtenu      : 10
  Nombre d'actions : 1

>>> L'optimal (cout 3) bat le satisficing (cout 10) : l'optimization trouve un plan 7 unites moins cher -- avec PLUS d'actions.

Interpretation : Optimisation vs satisfaction en planification

Sortie obtenue : fast-downward-opt (optimal) trouve le detour A -> B -> C -> D (3 actions, cout 3) et le certifie minimal ; fast-downward (satisficing) se contente du saut direct A -> D (1 action, cout 10). L’optimal bat le satisficing de 7 unites – avec plus d’actions. C’est la signature d’un probleme d’optimisation non-trivial : le cout distingue des plans de meme validite, et seul le moteur optimal explore l’espace assez pour trouver le moins cher.

Pourquoi c’est la capaute signature : - Sans quality metric, un planificateur se reduit a un chercheur de plan (BFS / GBFS sur le graphe d’etats) – l’optimalite n’est pas du tout recherchee. - Avec MinimizeActionCosts, le moteur devient un optimiseur combinatoire (A* sur le graphe pondere, heuristique admissible, garantie d’optimalite). - C’est exactement ce qui distingue un outils industriel (IPC optimal track) d’une demo pedagogique de recherche.

Miroir de la capabilite signature de CP-SAT (model.Maximize) et de Z3 (Optimize().maximize()) : dans les trois familles (planification, CP, SMT), l’optimization est le saut qualitatif au-dela de la satisfaction. Voir Sudoku-10-ORTools-Python (#7588) et Sudoku-12-Z3-Python (#7589).

Note technique : MinimizeActionCosts mappe chaque action a une expression de cout (ici edge_cost(l_from, l_to), un fluent numerique initialise par arete). D’autres metrics : MinimizeMakespan (planification temporelle – duree totale), MinimizeExpressionOnFinalState (minimiser une ressource residuelle), MaximizeExpressionOnFinalState (maximiser un gain).


5. Integration PDDL

unified-planning peut lire et ecrire du PDDL, permettant l’interoperabilite avec l’ecosysteme existant.

5.1 Export vers PDDL

La conversion vers PDDL permet d’utiliser des outils qui ne supportent que ce format.

# Export du probleme robot_navigation vers PDDL
from unified_planning.io import PDDLWriter

# Creation du writer
writer = PDDLWriter(problem)

# Generation du domaine PDDL
print("DOMAINE PDDL (domain.pddl):")
print("=" * 60)
domain_pddl = writer.get_domain()
print(domain_pddl)
DOMAINE PDDL (domain.pddl):
============================================================
(define (domain robot_navigation-domain)
 (:requirements :strips :typing)
 (:types robot location)
 (:predicates 
             (robot_at ?r - robot ?l - location)
             (connected ?l1 - location ?l2 - location)
             (visited ?l - location)
 )
 (:action move
  :parameters ( ?r - robot ?l_from - location ?l_to - location)
  :precondition (and (connected ?l_from ?l_to) (robot_at ?r ?l_from))
  :effect (and (not (robot_at ?r ?l_from)) (robot_at ?r ?l_to) (visited ?l_to)))
)

Generation et affichage du fichier PDDL du problème (problem.pddl) exporté, contenant les objets, l’etat initial et le but sous forme textuelle standardisee.

# Generation du probleme PDDL
print("PROBLEME PDDL (problem.pddl):")
print("=" * 60)
problem_pddl = writer.get_problem()
print(problem_pddl)
PROBLEME PDDL (problem.pddl):
============================================================
(define (problem robot_navigation-problem)
 (:domain robot_navigation-domain)
 (:objects
   robot1 - robot
   l0 l1 l2 l3 l4 l5 l6 l7 l8 l9 l10 l11 l12 l13 l14 - location
 )
 (:init
              (connected l0 l1)
              (connected l1 l0)
              (connected l1 l2)
              (connected l2 l1)
              (connected l2 l4)
              (connected l4 l2)
              (connected l4 l7)
              (connected l7 l4)
              (connected l7 l10)
              (connected l10 l7)
              (connected l10 l13)
              (connected l13 l10)
              (connected l10 l11)
              (connected l11 l10)
              (connected l13 l11)
              (connected l11 l13)
              (connected l1 l3)
              (connected l3 l1)
              (connected l4 l5)
              (connected l5 l4)
              (connected l2 l6)
              (connected l6 l2)
              (connected l7 l8)
              (connected l8 l7)
              (connected l2 l9)
              (connected l9 l2)
              (connected l10 l12)
              (connected l12 l10)
              (connected l13 l14)
              (connected l14 l13)
              (robot_at robot1 l0)
              (visited l0)
 )
 (:goal (and 
           (robot_at robot1 l11)
        )
 )
)

5.2 Import depuis PDDL

La lecture de fichiers PDDL permet de travailler avec des domaines existants.

# Exemple de lecture de fichiers PDDL
from unified_planning.io import PDDLReader

# Si vous avez des fichiers PDDL:
# reader = PDDLReader()
# problem = reader.parse_problem('domain.pddl', 'problem.pddl')

# Pour la demonstration, creons des fichiers temporaires
import tempfile
import os

# Creation de fichiers temporaires
with tempfile.TemporaryDirectory() as tmpdir:
    domain_path = os.path.join(tmpdir, 'domain.pddl')
    problem_path = os.path.join(tmpdir, 'problem.pddl')
    
    # Ecrire les fichiers
    with open(domain_path, 'w') as f:
        f.write(domain_pddl)
    with open(problem_path, 'w') as f:
        f.write(problem_pddl)
    
    # Relire les fichiers
    reader = PDDLReader()
    reloaded_problem = reader.parse_problem(domain_path, problem_path)
    
    print(f"Probleme recharge: {reloaded_problem.name}")
    print(f"  Actions: {len(reloaded_problem.actions)}")
    print(f"  Objets: {len(reloaded_problem.all_objects)}")
    print(f"  Buts: {len(reloaded_problem.goals)}")
    
    # Verification que le probleme est identique
    print(f"\nVerification: probleme original vs recharge")
    print(f"  Meme nombre d'actions: {len(problem.actions) == len(reloaded_problem.actions)}")
    print(f"  Meme nombre d'objets: {len(problem.all_objects) == len(reloaded_problem.all_objects)}")
Probleme recharge: robot_navigation-problem
  Actions: 1
  Objets: 16
  Buts: 1

Verification: probleme original vs recharge
  Meme nombre d'actions: True
  Meme nombre d'objets: True

Interpretation : Integration PDDL

Quand utiliser unified-planning vs PDDL direct ?

Situation Approche recommandee
Prototypage rapide unified-planning (API Python)
Integration avec code Python unified-planning
Utilisation de domaines existants Import PDDL -> unified-planning
Soumission a competitions IPC Export PDDL depuis unified-planning
Debug pas a pas unified-planning (introspection Python)
Performance maximale PDDL direct avec Fast Downward

Points cles : 1. PDDLWriter/Reader assure la compatibilite avec l’ecosysteme PDDL 2. L’API Python est plus flexible pour la generation dynamique de problemes 3. Certains commentaires et structures peuvent ne pas survivre a la conversion aller-retour


6. Bonnes Pratiques

6.1 Choix du solveur

Selon le type de problème, certains solveurs sont plus adaptes :

# Guide de sélection
def select_solver(problem: Problem) -> str:
    """Recommande un solveur selon les caractéristiques du problème."""
    kind = problem.kind
    
    if kind.has_continuous_time() or kind.has_discrete_time():
        return 'tamer'  # Planification temporelle
    elif kind.has_numeric_fluents():
        return 'enhsp'  # Fluents numériques
    elif kind.has_actions_cost():
        return 'fast-downward-opt'  # Optimal avec couts
    else:
        return 'pyperplan'  # Classique, rapide

6.2 Gestion des erreurs

Toujours gerer les cas ou le planificateur echoue :

result = planner.solve(problem)

if result.status == PlanGenerationResultStatus.SOLVED_SATISFICING:
    print(f"Plan trouve: {result.plan}")
elif result.status == PlanGenerationResultStatus.UNSOLVABLE_PROVEN:
    print("Problème prouve insolvable")
elif result.status == PlanGenerationResultStatus.TIMEOUT:
    print("Timeout atteint")
else:
    print(f"Statut inconnu: {result.status}")
# Exemple complet avec gestion d'erreurs et statistiques

def solve_robust(problem, preferred_solver='pyperplan', fallback_solver='fast-downward', timeout=30):
    """Resolution robuste avec fallback et statistiques detaillees."""
    
    solvers_to_try = [preferred_solver, fallback_solver]
    
    for solver_name in solvers_to_try:
        try:
            print(f"\nEssai avec {solver_name}...")
            
            with OneshotPlanner(name=solver_name) as planner:
                result = planner.solve(problem)
            
            # Analyser le resultat
            status = result.status
            
            if status == PlanGenerationResultStatus.SOLVED_SATISFICING:
                print(f"  SUCCES: Plan trouve!")
                print(f"  Longueur: {len(result.plan.actions)} actions")
                
                # Afficher les statistiques si disponibles
                if hasattr(result, 'statistics') and result.statistics:
                    print(f"  Statistiques: {result.statistics}")
                
                return result.plan
            
            elif status == PlanGenerationResultStatus.SOLVED_OPTIMALLY:
                print(f"  SUCCES: Plan optimal trouve!")
                return result.plan
            
            elif status == PlanGenerationResultStatus.UNSOLVABLE_PROVEN:
                print(f"  ECHEC: Probleme insolvable")
                return None
            
            elif status == PlanGenerationResultStatus.TIMEOUT:
                print(f"  ECHEC: Timeout")
                continue  # Essayer le suivant
            
            else:
                print(f"  ECHEC: {status}")
                continue
                
        except Exception as e:
            print(f"  ERREUR: {e}")
            continue
    
    print("\nAucun solveur n'a reussi")
    return None

# Test de la fonction robuste
plan = solve_robust(problem)

Essai avec pyperplan...
  SUCCES: Plan trouve!
  Longueur: 6 actions

Interpretation : Resolution robuste avec gestion d’erreurs

Sortie obtenue : le solveur preferé (pyperplan) reussit du premier coup : SUCCES: Plan trouve! Longueur: 6 actions. La fonction solve_robust retourne immediatement le plan, sans avoir besoin d’invoquer le solveur de fallback (fast-downward).

Solveur Statut Plan
pyperplan (preferé) SUCCES 6 actions
fast-downward (fallback) non invoqué –

Lecon : avec les deux solveurs installes (up-pyperplan, up-fast-downward), pyperplan resout le labyrinthe tout seul. L’interet du pattern solveur-preferé + fallback est precisement de rester robuste meme quand le solveur preferé est absent ou echoue : le code essaie pyperplan, et seulement s’il echoue bascule sur fast-downward. L’exécution robuste ne signifie pas que tous les solveurs doivent reussir, mais qu’au moins un degrade gracieusement vers une solution.

Workflow robuste : 1. Essayer le solveur preferé (ici pyperplan, leger) 2. En cas d’echec, essayer le solveur de fallback (ici fast-downward) 3. Gerer les différents statuts (SOLVED, UNSOLVABLE, TIMEOUT) 4. Afficher des statistiques detaillees si disponibles

Note technique : Dans un environnement de production, il est recommande d’installer les solveurs via les packages UP (up-fast-downward, up-pyperplan) plutot que d’utiliser les installations système, ce qui garantit la compatibilite des versions. Si pyperplan n’etait pas installe, c’est fast-downward qui aurait produit le plan : le code de fallback est pret, simplement non declenche sur cette instance.


7. Resume et Points Cles

7.1 Concepts appris

Concept Description
unified-planning Interface Python unifiee pour multiples planificateurs
Fluent Predicat ou fonction variable selon l’etat
InstantaneousAction Action avec preconditions/effets instantanes
DurativeAction Action avec duree et effets temporises
OneshotPlanner Planificateur one-shot (un problème, un plan)
PDDLWriter/Reader Conversion entre API Python et PDDL

Exercice : Planification temporelle avec ressources numériques

Contexte : Gestion d’un chantier de construction

Un chantier a 3 tâches a effectuer : fondations, murs, toiture. Certaines tâches ont des dependances et consomment des ressources.

Contraintes : - murs ne peut commencer qu’après la fin de fondations - toiture ne peut commencer qu’après la fin de murs - Chaque tâche consume du budget : fondations = 3000€, murs = 5000€, toiture = 2000€ - Budget total disponible : 12000€ - Durees : fondations = 5 jours, murs = 10 jours, toiture = 3 jours

Objectifs

  1. Modeliser ce problème comme un problème de planification temporelle avec unified_planning
  2. Définir un fluent numérique budget_restant initialise a 12000
  3. Définir 3 actions duratives (DurativeAction) avec contraintes de temps et effets numériques
  4. Resoudre avec un planificateur temporel (Tamer ou similaire)
  5. Afficher le plan avec les temps de debut/fin de chaque action
  6. (Bonus) Exporter le problème en PDDL avec PDDLWriter et verifier le fichier genere

Indices (API utile) :

  • DurativeAction + set_fixed_duration(...) pour les actions a duree
  • add_condition(StartTiming(), ...) / add_condition(EndTiming(), ...) pour les conditions temporisees
  • add_decrease_effect(EndTiming(), ...) pour la consommation de budget
  • Sequencez les tâches via des fluents booléens (ex: fondations_ok) utilises en precondition

Indice : Exercice de planification temporelle

Approche suggeree pour modeliser le problème de chantier :

  1. Fluents booléens pour etat des tâches :
    • fondations_ok, murs_ok, toiture_ok indiquent si chaque tâche est terminee
    • Ces fluents servent de preconditions pour les tâches suivantes
  2. Fluent numérique pour budget :
    • budget_restant initialise a 12000
    • Chaque action diminue le budget (effet numérique)
  3. Actions duratives (DurativeAction) :
    • set_fixed_duration(jours) pour chaque tâche
    • Conditions au debut (StartTiming()) : verifier budget suffisant
    • Conditions au debut (StartTiming()) : verifier dependances (ex: murs_ok pour toiture)
    • Effets a la fin (EndTiming()) : marquer tâche OK, diminuer budget
  4. Resolution :
    • Utiliser un planificateur supportant le temporel (Tamer, ENHSP)
    • Le plan devrait montrer les temps de debut/fin de chaque tâche

Note : Les dependances entre tâches peuvent se modeliser de deux facons : (1) avec un fluent booléen (fondations_ok=True) ou (2) avec une contrainte de timing (start_murs >= end_fondations). L’approche (1) est plus flexible.

# --- Exercice : Chantier de construction - Planification temporelle ---
# Modelisez le chantier (fondations -> murs -> toiture) avec des DurativeAction
# (durees, conditions / effets temporises) et un budget (fluent numerique).
# Resolvez avec un planificateur temporel.
from unified_planning.shortcuts import *

# A vous de jouer.
print("Exercice a completer")
Exercice a completer

7.2 Points cles a retenir

  1. Abstraction : unified-planning permet d’ecrire le problème une fois, de le resoudre avec n’importe quel solveur

  2. Comparaison : Facilite le benchmarking de différents planificateurs

  3. Extensibilite : Supporte PDDL classique, temporel, et numérique

  4. Interoperabilite : Import/export PDDL pour compatibilite avec l’ecosysteme existant

  5. Choix du solveur : Adapter le solveur au type de problème (classique vs temporel vs numérique)

7.3 Quand utiliser unified-planning ?

Cas d’usage Recommandation
Recherche / Prototypage unified-planning (flexibilite)
Production (performance) PDDL direct + Fast Downward
Integration Python unified-planning
Benchmark multi-solveurs unified-planning
Domaines IPC existants Import PDDL -> unified-planning

Ressources


Notebook suivant : Planners-12-LOOP - Learning to Plan avec modèles neuronaux

Resume et perspectives

Ce notebook a presente unified-planning, une bibliotheque Python qui unifie l’acces a de multiples planificateurs sous une API coherente. Nous avons défini des problemes de planification directement en Python (types, fluents, actions, preconditions, effets), resolu ces problemes avec différents solveurs (Fast Downward, Pyperplan), et compare leurs performances en termes de temps de resolution et qualite de plan. Les fonctionnalites avancees – actions duratives, fluents numériques, contraintes de duree et de ressources – montrent la richesse expressive de l’API au-dela du simple modèle STRIPS. L’integration PDDL via PDDLWriter et PDDLReader assure la compatibilite avec l’ecosysteme de planification existant.

L’intérêt principal d’unified-planning reside dans l’abstraction qu’il offre : un problème défini une seule fois peut etre resolu par n’importe quel solveur compatible, ce qui facilite considerablement le benchmarking et la sélection du solveur optimal pour un domaine donne. La gestion robuste des erreurs (fallback entre solveurs, gestion des timeouts, statuts detailles) est essentielle en production, ou la fiabilite prime sur la performance pure. Le choix du solveur doit etre guide par les caractéristiques du problème : planification classique pour Pyperplan ou Fast Downward, fluents numériques pour ENHSP, planification temporelle pour LPG ou Tamer.

Le notebook suivant, Planners-12-LOOP, explore le paradigme Learning to Plan, ou des reseaux de neurones apprennent des heuristiques de planification a partir de données, complementant les approches symboliques etudiees ici.


Exercice de synthese : Comparer deux solveurs sur un problème original

Utilisez unified-planning pour resoudre votre propre problème avec deux solveurs différents et comparez leurs performances.

Contraintes

  • Problème original (pas Block World ni Transport)
  • Tester avec au moins 2 solveurs (ex: pyperplan + tamer ou enhsp)
  • Mesurer et comparer le temps de resolution et la longueur du plan

Étapes : 1. Définir un problème de planification original avec unified_planning 2. Resoudre avec OneshotPlanner pour chaque solveur 3. Comparer les résultats : statut, longueur du plan, temps 4. (Bonus) Utiliser AnytimePlanner pour obtenir des plans sous-optimaux progressifs

# TODO etudiant : comparer deux solveurs sur un probleme original
import time
from unified_planning.shortcuts import *

# Demarche : (1) definir un probleme original (ni Block World ni Transport),
# (2) le resoudre avec deux solveurs differents (ex: pyperplan, tamer, enhsp,
# fast-downward) en mesurant temps + longueur de plan, (3) comparer les resultats.

print("Exercice a completer")
Exercice a completer
Retour au sommet