Planners-9-HTN - Planification Hiérarchique

Navigation : Index | << Temporal | LLM-Planning >>


Objectifs d’apprentissage

A la fin de ce notebook, vous saurez :

  1. Comprendre le paradigme de planification hiérarchique (HTN)
  2. Distinguer les tâches abstraites des tâches primitives
  3. Définir des méthodes de decomposition avec preconditions
  4. Implementer un solveur HTN simplifie
  5. Comparer HTN avec la planification classique STRIPS

Prerequis

  • Notebooks Planners-1 a 3 maitrises (PDDL, espaces d’etats)
  • Notebook Planners-4 (Fast Downward) recommande
  • Python 3.9+ avec unified-planning

Duree estimee : 45 minutes


1. Introduction a la Planification Hiérarchique

La planification hiérarchique (Hierarchical Task Network - HTN) est un paradigme de planification qui structure le problème en tâches pouvant etre decomposees recursivement. Contrairement a la planification classique qui cherche un chemin dans un espace d’etats, HTN cherche a decomposer une tâche complexe en actions primitives.

1.1 Motivation

Dans la planification classique STRIPS/PDDL : - L’objectif est un etat but (conjonction de predicats) - Le planificateur explore l’espace d’etats - Le domaine est “plat” : toutes les actions sont au même niveau

En planification HTN : - L’objectif est une tâche a accomplir - Le planificateur decompose les tâches - Le domaine est hiérarchique : tâches abstraites et primitives

1.2 Analogie : Planifier un voyage

Planification classique : On specifie l’etat final desire

But: a(Tokyo) ^ avoir_billet(Paris, Tokyo)

Planification HTN : On specifie la tâche a accomplir

Tâche: voyager(Paris, Tokyo)
  -> Méthode 1: voyager_en_avion
       sous-tâches: [reserver_billet, aller_aeroport, prendre_vol, sortir_aeroport]
  -> Méthode 2: voyager_en_train (si distance < 1000km)
       sous-tâches: [acheter_billet, aller_gare, prendre_train]

La connaissance de l’expert sur “comment voyager” est encodee dans les méthodes.

1.3 Comparaison conceptuelle

Aspect Planification classique Planification HTN
Objectif Etat but a atteindre Tâche a accomplir
Connaissance Actions atomiques Méthodes de decomposition
Recherche Dans l’espace d’etats Dans l’espace des decompositions
Expertise Encodee implicitement Encodee explicitement
Flexibilite Haute (n’importe quel plan) Guidee (plans “raisonnables”)

2. Fondements Théoriques des HTN

2.1 Composantes d’un domaine HTN

Un domaine HTN comprend :

  1. Predicats : Faits sur l’etat du monde (comme en PDDL)
  2. Actions primitives : Actions executables avec preconditions et effets
  3. Tâches abstraites : Tâches non-executables directement
  4. Méthodes : Règles de decomposition des tâches abstraites
Domaine HTN = <Predicats, Actions, Tâches, Méthodes>

2.2 Tâches primitives vs abstraites

Tâche primitive : Correspond a une action executable - A des preconditions et des effets - Est directement executable dans le monde - Exemple : !drive(truck, loc-a, loc-b)

Tâche abstraite : Requiert une decomposition - N’est pas directement executable - Peut etre decomposee de multiples facons (méthodes) - Exemple : deliver(package, loc-a, loc-b)

Notation : Par convention, les tâches primitives sont souvent prefixees par !.

2.3 Méthodes de decomposition

Une méthode définit comment decomposer une tâche abstraite :

\[m = \langle \text{name}, \text{task}, \text{precond}, \text{subtasks} \rangle\]

  • name : Identifiant de la méthode
  • task : Tâche abstraite a decomposer
  • precond : Preconditions pour que cette méthode soit applicable
  • subtasks : Liste ordonnee de sous-tâches (reseau de tâches)

Exemple : Méthode pour livrer un colis

Méthode: m_deliver_by_truck
  Tâche: deliver(pkg, from, to)
  Preconditions: 
    - truck-available(t)
    - connected(from, to)
  Sous-tâches:
    1. !load(pkg, t, from)
    2. !drive(t, from, to)
    3. !unload(pkg, t, to)

2.4 Reseaux de tâches (Task Networks)

Un reseau de tâches est un ensemble partiellement ordonne de tâches avec des contraintes :

  • Tâches : Ensemble de tâches (primitives ou abstraites)
  • Ordre : Contraintes de precedence (\(t_1 \prec t_2\))
  • Contraintes : Relations sur les variables
Task Network pour "construire-maison":
  Tâches: {fondation, murs, toit, fenêtres}
  Ordre: fondation < murs < toit, murs < fenêtres

La decomposition progressive du reseau de tâches initial produit un plan.


3. HDDL - Hierarchical Domain Definition Language

HDDL est une extension de PDDL pour la planification hiérarchique. Il ajoute la syntaxe pour définir des tâches et des méthodes.

3.1 Structure d’un domaine HDDL

(define (domain logistics-htn)
  (:requirements :strips :typing :hierarchy)  ;; :hierarchy pour HTN
  
  ;; Types et predicats (comme PDDL)
  (:types location package vehicle)
  (:predicates (at ?p - package ?l - location) ...)
  
  ;; Actions primitives
  (:action drive :parameters (?v - vehicle ?from ?to - location) ...)
  
  ;; Tâches (declarees explicitement)
  (:task deliver :parameters (?p - package ?from ?to - location))
  
  ;; Méthodes
  (:method m_deliver
    :parameters (?p - package ?from ?to - location ?t - vehicle)
    :task (deliver ?p ?from ?to)
    :precondition (and (at-vehicle ?t ?from) ...)
    :subtasks (and 
               (load ?p ?t ?from)
               (drive ?t ?from ?to)
               (unload ?p ?t ?to))
  )
)

3.2 Exemple complet : Domaine Logistics HTN

# Definition du domaine HDDL pour Logistics
HDDL_DOMAIN = """
(define (domain logistics-htn)
  (:requirements :strips :typing :hierarchy)
  (:types location package vehicle)
  
  (:predicates
    (at-pkg ?p - package ?l - location)
    (at-veh ?v - vehicle ?l - location)
    (in ?p - package ?v - vehicle)
    (connected ?from ?to - location)
  )
  
  ;; ========== Actions primitives ==========
  
  (:action load
    :parameters (?p - package ?v - vehicle ?l - location)
    :precondition (and (at-pkg ?p ?l) (at-veh ?v ?l))
    :effect (and (in ?p ?v) (not (at-pkg ?p ?l)))
  )
  
  (:action unload
    :parameters (?p - package ?v - vehicle ?l - location)
    :precondition (in ?p ?v)
    :effect (and (at-pkg ?p ?l) (not (in ?p ?v)))
  )
  
  (:action drive
    :parameters (?v - vehicle ?from ?to - location)
    :precondition (and (at-veh ?v ?from) (connected ?from ?to))
    :effect (and (at-veh ?v ?to) (not (at-veh ?v ?from)))
  )
  
  ;; ========== Tâches abstraites ==========
  
  (:task deliver
    :parameters (?p - package ?from ?to - location)
  )
  
  ;; ========== Méthodes ==========
  
  (:method m_deliver_direct
    :parameters (?p - package ?from ?to - location ?v - vehicle)
    :task (deliver ?p ?from ?to)
    :precondition (and 
                    (at-veh ?v ?from) 
                    (connected ?from ?to))
    :subtasks (and
                (task (load ?p ?v ?from))
                (task (drive ?v ?from ?to))
                (task (unload ?p ?v ?to))
              )
  )
  
  (:method m_deliver_via_hub
    :parameters (?p - package ?from ?hub ?to - location ?v1 ?v2 - vehicle)
    :task (deliver ?p ?from ?to)
    :precondition (and
                    (at-veh ?v1 ?from)
                    (at-veh ?v2 ?hub)
                    (connected ?from ?hub)
                    (connected ?hub ?to)
                    (not (= ?from ?hub))
                    (not (= ?hub ?to))
                  )
    :subtasks (and
                (task (load ?p ?v1 ?from))
                (task (drive ?v1 ?from ?hub))
                (task (unload ?p ?v1 ?hub))
                (task (load ?p ?v2 ?hub))
                (task (drive ?v2 ?hub ?to))
                (task (unload ?p ?v2 ?to))
              )
  )
)
"""

print("Domaine HDDL défini: logistics-htn")
print("\nActions primitives: load, unload, drive")
print("Tâches abstraites: deliver")
print("Méthodes: m_deliver_direct, m_deliver_via_hub")
Domaine HDDL défini: logistics-htn

Actions primitives: load, unload, drive
Tâches abstraites: deliver
Méthodes: m_deliver_direct, m_deliver_via_hub

Interpretation : Domaine HDDL Logistics

Le domaine HDDL défini illustre la structure hiérarchique typique en HTN.

Analyse des composantes :

Élément Description
Actions primitives load, unload, drive - opérations atomiques exécutables
Tâche abstraite deliver(pkg, from, to) - opération de haut niveau à décomposer
Méthode directe m_deliver_direct - 1 véhicule, 3 sous-tâches, connexion directe
Méthode via hub m_deliver_via_hub - 2 véhicules, 6 sous-tâches, via point intermédiaire

Points clés de la modélisation : - Les méthodes ont des préconditions différentes (choix guidé par l’état) - via_hub nécessite deux véhicules et un hub de transit - Les sous-tâches sont ordonnées (séquence logique : load → drive → unload)

Pourquoi deux méthodes ? - Si une connexion directe existe : m_deliver_direct est plus simple - Si pas de connexion directe : m_deliver_via_hub permet de passer par un hub - Le solveur choisira automatiquement la méthode applicable selon l’état

Note : HDDL étend PDDL avec les mot-clés :task, :method, :subtasks et la requirement :hierarchy. La syntaxe reste familière aux utilisateurs de PDDL classique.

3.3 Definition du problème HDDL

Un problème HDDL specifie le reseau de tâches initial plutot qu’un etat but.

# Definition du problème HDDL
HDDL_PROBLEM = """
(define (problem logistics-htn-1)
  (:domain logistics-htn)
  (:objects
    depot warehouse store - location
    pkg1 pkg2 - package
    truck1 - vehicle
  )
  
  (:init
    ;; Positions initiales des colis
    (at-pkg pkg1 depot)
    (at-pkg pkg2 warehouse)
    
    ;; Position initiale du camion
    (at-veh truck1 depot)
    
    ;; Connexions entre lieux
    (connected depot warehouse)
    (connected warehouse store)
    (connected depot store)
    (connected warehouse depot)
    (connected store warehouse)
    (connected store depot)
  )
  
  ;; Tâche initiale (au lieu de :goal)
  (:htn
    :subtasks (and
                (task (deliver pkg1 depot store))
                (task (deliver pkg2 warehouse depot))
              )
  )
)
"""

print("Problème HDDL défini: logistics-htn-1")
print("\nTaches initiales:")
print("  1. deliver(pkg1, depot, store)")
print("  2. deliver(pkg2, warehouse, depot)")
Problème HDDL défini: logistics-htn-1

Taches initiales:
  1. deliver(pkg1, depot, store)
  2. deliver(pkg2, warehouse, depot)

Interpretation du domaine HDDL

Élément Description
Actions load, unload, drive - opérations atomiques
Tâche abstraite deliver(pkg, from, to) - a decomposer
m_deliver_direct Méthode avec 1 vehicule, 3 sous-tâches
m_deliver_via_hub Méthode avec 2 vehicules via hub, 6 sous-tâches

Note : La méthode via_hub est utile quand il n’y a pas de connexion directe ou quand différentes compagnies gerent différents segments.


4. Implementation d’un Solveur HTN Simplifie

Implementons un solveur HTN simple en Python pour comprendre les mécanismes de decomposition.

# Implementation d'un solveur HTN simplifie
from dataclasses import dataclass, field
from typing import List, Dict, Set, Tuple, Optional, Any, Union
from copy import deepcopy

@dataclass
class State:
    """Representation d'un etat du monde."""
    facts: Set[Tuple[str, ...]]  # Ensemble de faits (predicats)
    
    def holds(self, predicate: Tuple[str, ...]) -> bool:
        """Verifie si un fait est vrai dans l'etat."""
        return predicate in self.facts
    
    def add(self, predicate: Tuple[str, ...]) -> 'State':
        """Ajoute un fait et retourne un nouvel etat."""
        new_facts = self.facts | {predicate}
        return State(new_facts)
    
    def remove(self, predicate: Tuple[str, ...]) -> 'State':
        """Retire un fait et retourne un nouvel etat."""
        new_facts = self.facts - {predicate}
        return State(new_facts)
    
    def __repr__(self):
        return f"State({sorted(self.facts)})"


@dataclass
class Action:
    """Action primitive avec preconditions et effets."""
    name: str
    parameters: List[str]
    preconditions: List[Tuple[str, ...]]
    add_effects: List[Tuple[str, ...]]
    del_effects: List[Tuple[str, ...]]
    
    def is_applicable(self, state: State, binding: Dict[str, str]) -> bool:
        """Verifie si l'action est applicable avec les bindings donnes."""
        for precond in self.preconditions:
            ground = tuple(binding.get(p, p) for p in precond)
            if not state.holds(ground):
                return False
        return True
    
    def apply(self, state: State, binding: Dict[str, str]) -> State:
        """Applique l'action et retourne le nouvel etat."""
        new_state = state
        for eff in self.del_effects:
            ground = tuple(binding.get(p, p) for p in eff)
            new_state = new_state.remove(ground)
        for eff in self.add_effects:
            ground = tuple(binding.get(p, p) for p in eff)
            new_state = new_state.add(ground)
        return new_state
    
    def ground(self, binding: Dict[str, str]) -> str:
        """Retourne la representation groundee de l'action."""
        params = [binding.get(p, p) for p in self.parameters]
        return f"{self.name}({', '.join(params)})"


@dataclass
class Method:
    """Méthode de decomposition pour tâche abstraite."""
    name: str
    task_name: str
    task_params: List[str]
    parameters: List[str]
    preconditions: List[Tuple[str, ...]]
    subtasks: List[Tuple[str, List[str]]]  # (nom_tache, params)
    
    def is_applicable(self, state: State, binding: Dict[str, str]) -> bool:
        """Verifie si la méthode est applicable."""
        for precond in self.preconditions:
            ground = tuple(binding.get(p, p) for p in precond)
            if not state.holds(ground):
                return False
        return True

print("Classes de base définies: State, Action, Method")
Classes de base définies: State, Action, Method

4.1 Classes de base pour HTN

Les classes State, Action, et Method forment le coeur de notre representation HTN :

Classe Rôle
State Represente l’etat du monde comme un ensemble de faits
Action Action primitive avec preconditions et effets (add/delete)
Method Règle de decomposition avec preconditions et sous-tâches

Le code suivant définit ces structures de données.

# Solveur HTN simplifie
from itertools import product as itertools_product

@dataclass
class HTNPlanner:
    """Solveur HTN avec decomposition ordonnee."""
    actions: Dict[str, Action]
    methods: Dict[str, List[Method]]
    
    def solve(self, initial_state: State, 
              tasks: List[Tuple[str, List[str]]],
              binding: Dict[str, str] = None) -> Optional[List[str]]:
        """
        Resout le problème HTN par decomposition.
        
        Args:
            initial_state: Etat initial
            tasks: Liste de tâches (primitives ou abstraites)
            binding: Bindings des variables
        
        Returns:
            Liste d'actions groundees ou None si echec
        """
        if binding is None:
            binding = {}
        
        return self._decompose(initial_state, tasks, binding, [])
    
    def _decompose(self, state: State, 
                   tasks: List[Tuple[str, List[str]]],
                   binding: Dict[str, str],
                   plan: List[str]) -> Optional[List[str]]:
        """Decomposition récursive des tâches."""
        
        # Cas de base: plus de tâches
        if not tasks:
            return plan
        
        current_task, current_params = tasks[0]
        remaining_tasks = tasks[1:]
        
        # Ground les paramètres
        grounded_params = [binding.get(p, p) for p in current_params]
        
        # Cas 1: Tâche primitive
        if current_task in self.actions:
            action = self.actions[current_task]
            action_binding = {p: v for p, v in zip(action.parameters, grounded_params)}
            
            if action.is_applicable(state, action_binding):
                new_state = action.apply(state, action_binding)
                action_str = action.ground(action_binding)
                return self._decompose(new_state, remaining_tasks, binding, plan + [action_str])
            else:
                # Action non applicable - echec
                return None
        
        # Cas 2: Tâche abstraite - essayer les méthodes
        if current_task in self.methods:
            for method in self.methods[current_task]:
                # Créer le binding pour les paramètres de la tâche
                method_binding = binding.copy()
                for tp, gp in zip(method.task_params, grounded_params):
                    method_binding[tp] = gp
                
                # Trouver les bindings pour les paramètres supplementaires de la méthode
                # (ceux qui ne sont pas dans les paramètres de la tâche)
                task_param_set = set(method.task_params)
                extra_params = [p for p in method.parameters if p not in task_param_set]
                
                if extra_params:
                    # Chercher tous les objets dans l'etat qui pourraient convenir
                    all_objects = set()
                    for fact in state.facts:
                        all_objects.update(fact[1:])  # Tous les objets (sauf le predicat)
                    all_objects = sorted(all_objects)  # tri déterministe
                    
                    # Essayer chaque combinaison d'objets pour les paramètres supplementaires
                    for combo in itertools_product(all_objects, repeat=len(extra_params)):
                        extended_binding = method_binding.copy()
                        for param, obj in zip(extra_params, combo):
                            extended_binding[param] = obj
                        
                        # Verifier les preconditions avec ce binding etendu
                        if method.is_applicable(state, extended_binding):
                            # Ground les sous-tâches
                            grounded_subtasks = []
                            for st_name, st_params in method.subtasks:
                                grounded = [extended_binding.get(p, p) for p in st_params]
                                grounded_subtasks.append((st_name, grounded))
                            
                            # Essayer avec cette decomposition
                            new_tasks = grounded_subtasks + remaining_tasks
                            result = self._decompose(state, new_tasks, extended_binding, plan)
                            if result is not None:
                                return result
                else:
                    # Pas de paramètres supplementaires
                    if method.is_applicable(state, method_binding):
                        # Ground les sous-tâches
                        grounded_subtasks = []
                        for st_name, st_params in method.subtasks:
                            grounded = [method_binding.get(p, p) for p in st_params]
                            grounded_subtasks.append((st_name, grounded))
                        
                        # Essayer avec cette decomposition
                        new_tasks = grounded_subtasks + remaining_tasks
                        result = self._decompose(state, new_tasks, method_binding, plan)
                        if result is not None:
                            return result
            
            # Aucune méthode n'a fonctionne
            return None
        
        # Tâche inconnue
        print(f"ERREUR: Tâche inconnue: {current_task}")
        return None

print("Solveur HTN défini: HTNPlanner")
Solveur HTN défini: HTNPlanner

Interpretation : Architecture du solveur HTN

La classe HTNPlanner implémente l’algorithme central de décomposition hiérarchique.

Composantes principales :

Méthode Rôle
solve() Point d’entrée, lance la décomposition
_decompose() Fonction récursive qui traite les tâches une par une
Vérification des préconditions Teste si une action/méthode est applicable

Logique de décomposition : 1. Tâche primitive : Vérifier les préconditions, appliquer l’effet, continuer 2. Tâche abstraite : Essayer chaque méthode applicable dans l’ordre 3. Binding de variables : Les paramètres sont substitués par les objets concrets 4. Backtracking : Si une méthode échoue, essayer la suivante

Points clés de l’implémentation : - L’état est immuable (chaque action crée un nouvel état) - Les paramètres supplémentaires des méthodes sont liés aux objets disponibles - L’ordre des tâches est préservé (ordered decomposition)

Note technique : Cette implémentation simplifiée ne gère pas les contraintes d’ordering entre sous-tâches (elles sont exécutées séquentiellement). SHOP2 complet supporte des réseaux de tâches partiellement ordonnés.


5. Exemple Pratique : Logistics avec HTN

Appliquons notre solveur HTN au domaine Logistics.

# Definition du domaine HTN pour Logistics

# Actions primitives
actions = {
    'load': Action(
        name='load',
        parameters=['?p', '?v', '?l'],
        preconditions=[('at-pkg', '?p', '?l'), ('at-veh', '?v', '?l')],
        add_effects=[('in', '?p', '?v')],
        del_effects=[('at-pkg', '?p', '?l')]
    ),
    'unload': Action(
        name='unload',
        parameters=['?p', '?v', '?l'],
        preconditions=[('in', '?p', '?v'), ('at-veh', '?v', '?l')],
        add_effects=[('at-pkg', '?p', '?l')],
        del_effects=[('in', '?p', '?v')]
    ),
    'drive': Action(
        name='drive',
        parameters=['?v', '?from', '?to'],
        preconditions=[('at-veh', '?v', '?from'), ('connected', '?from', '?to')],
        add_effects=[('at-veh', '?v', '?to')],
        del_effects=[('at-veh', '?v', '?from')]
    )
}

# Méthodes pour la tâche abstraite 'deliver'
methods = {
    'deliver': [
        Method(
            name='m_deliver_direct',
            task_name='deliver',
            task_params=['?p', '?from', '?to'],
            parameters=['?p', '?from', '?to', '?v'],
            preconditions=[
                ('at-veh', '?v', '?from'),
                ('connected', '?from', '?to')
            ],
            subtasks=[
                ('load', ['?p', '?v', '?from']),
                ('drive', ['?v', '?from', '?to']),
                ('unload', ['?p', '?v', '?to'])
            ]
        ),
        Method(
            name='m_deliver_via_intermediate',
            task_name='deliver',
            task_params=['?p', '?from', '?to'],
            parameters=['?p', '?from', '?mid', '?to', '?v'],
            preconditions=[
                ('at-veh', '?v', '?from'),
                ('connected', '?from', '?mid'),
                ('connected', '?mid', '?to')
            ],
            subtasks=[
                ('load', ['?p', '?v', '?from']),
                ('drive', ['?v', '?from', '?mid']),
                ('drive', ['?v', '?mid', '?to']),
                ('unload', ['?p', '?v', '?to'])
            ]
        )
    ]
}

print("Domaine HTN Logistics défini")
print(f"Actions: {list(actions.keys())}")
print(f"Méthodes pour 'deliver': {[m.name for m in methods['deliver']]}")
Domaine HTN Logistics défini
Actions: ['load', 'unload', 'drive']
Méthodes pour 'deliver': ['m_deliver_direct', 'm_deliver_via_intermediate']

Definition du problème logistique concret : etat initial avec les positions des colis et vehicules, but a atteindre, puis resolution par decomposition HTN.

# Definition du problème et resolution

# Etat initial
initial_facts = {
    # Positions des colis
    ('at-pkg', 'pkg1', 'depot'),
    ('at-pkg', 'pkg2', 'warehouse'),
    
    # Position du camion
    ('at-veh', 'truck1', 'depot'),
    
    # Connexions (graphe non oriente)
    ('connected', 'depot', 'warehouse'),
    ('connected', 'warehouse', 'depot'),
    ('connected', 'warehouse', 'store'),
    ('connected', 'store', 'warehouse'),
    ('connected', 'depot', 'store'),
    ('connected', 'store', 'depot'),
}

initial_state = State(initial_facts)

# Tâche unique a accomplir (pour simplifier l'exemple)
tasks = [
    ('deliver', ['pkg1', 'depot', 'store']),
]

# Creation du solveur
planner = HTNPlanner(actions=actions, methods=methods)

# Resolution
print("=" * 60)
print("RESOLUTION HTN - Exemple 1")
print("=" * 60)
print(f"\nEtat initial:")
print(f"  pkg1 au depot, pkg2 au warehouse")
print(f"  truck1 au depot")
print(f"\nTache:")
print(f"  1. deliver(pkg1, depot, store)")
print("\n" + "-" * 60)

plan = planner.solve(initial_state, tasks)

if plan:
    print("\nPLAN TROUVE:")
    print("=" * 60)
    for i, action in enumerate(plan, 1):
        print(f"  {i}. {action}")
    print("=" * 60)
    print(f"Longueur du plan: {len(plan)} actions")
else:
    print("ECHEC: Aucun plan trouve")

# Deuxieme exemple: livrer depuis le warehouse
print("\n\n" + "=" * 60)
print("RESOLUTION HTN - Exemple 2")
print("=" * 60)

# Pour livrer pkg2 depuis warehouse, on doit d'abord deplacer le camion
# Ce n'est pas possible avec nos méthodes actuelles
print(f"\nRemarque: Le deuxieme colis pkg2 est au warehouse.")
print(f"Après la première livraison, truck1 est au 'store'.")
print(f"Pour livrer pkg2, il faudrait une méthode de repositionnement.")
============================================================
RESOLUTION HTN - Exemple 1
============================================================

Etat initial:
  pkg1 au depot, pkg2 au warehouse
  truck1 au depot

Tache:
  1. deliver(pkg1, depot, store)

------------------------------------------------------------

PLAN TROUVE:
============================================================
  1. load(pkg1, truck1, depot)
  2. drive(truck1, depot, store)
  3. unload(pkg1, truck1, store)
============================================================
Longueur du plan: 3 actions


============================================================
RESOLUTION HTN - Exemple 2
============================================================

Remarque: Le deuxieme colis pkg2 est au warehouse.
Après la première livraison, truck1 est au 'store'.
Pour livrer pkg2, il faudrait une méthode de repositionnement.

Interpretation : Résolution HTN réussie

Le solveur HTN a trouvé un plan de 3 actions pour livrer pkg1 du depot au store.

Analyse de la solution :

Composant Rôle
Méthode sélectionnée m_deliver_direct - car le camion est déjà au depot
Sous-tâches générées 3 actions primitives : load, drive, unload
Vérification des préconditions Chaque action vérifie l’état avant exécution
Longueur du plan 3 actions (optimal pour ce cas simple)

Points clés : 1. Le solveur n’a pas eu besoin d’explorer l’espace d’états complet 2. La méthode m_deliver_direct était directement applicable 3. La méthode m_deliver_via_intermediate n’a pas été essayée (inutile ici) 4. La décomposition est ordonnée : les tâches sont exécutées séquentiellement

Limitation notée : Le solveur simplifié ne gère pas le repositionnement automatique du véhicule. Pour livrer pkg2 (depuis warehouse), il faudrait une méthode reposition_vehicle ou que le camion y soit déjà.

Note technique : SHOP2 utilise un algorithme de “backtracking” sur les méthodes. Si m_deliver_direct échouait, il essayerait m_deliver_via_intermediate.

5.3 Démonstration : sélection de méthode par backtracking

Les deux méthodes de décomposition de la tâche deliver (m_deliver_direct et m_deliver_via_intermediate) constituent le cœur de l’expressivité HTN : le planificateur ne se contente pas de « développer » une recette fixe, il recherche parmi les méthodes applicables et revient en arrière (backtracking) si celle qu’il essaie mène à une impasse.

L’Exemple 1 ci-dessus livrait pkg1 du dépôt au magasin avec une liaison directe depot -> store : la méthode m_deliver_direct était applicable et donnait un plan de 3 actions. La méthode m_deliver_via_intermediate n’était jamais sollicitée.

Pour rendre visible la sélection de méthode, retirons la liaison directe depot <-> store : la méthode directe devient inapplicable, et le planificateur doit backtracker vers m_deliver_via_intermediate (passage par l’entrepôt).

# Demonstration : selection de methode par backtracking
# On retire la connexion directe depot <-> store : m_deliver_direct devient
# inapplicable, forcant le planificateur a backtracker vers m_deliver_via_intermediate.

initial_facts_nodirect = {
    ('at-pkg', 'pkg1', 'depot'),
    ('at-pkg', 'pkg2', 'warehouse'),
    ('at-veh', 'truck1', 'depot'),
    # Graphe : depot <-> warehouse <-> store  (PAS de liaison depot <-> store directe)
    ('connected', 'depot', 'warehouse'),
    ('connected', 'warehouse', 'depot'),
    ('connected', 'warehouse', 'store'),
    ('connected', 'store', 'warehouse'),
}

state_nodirect = State(initial_facts_nodirect)
tasks_nodirect = [('deliver', ['pkg1', 'depot', 'store'])]

planner_bs = HTNPlanner(actions=actions, methods=methods)
plan_bs = planner_bs.solve(state_nodirect, tasks_nodirect)

# Infere la methode selectionnee a partir de la structure du plan (nombre de drive)
n_drives = sum(1 for a in plan_bs if a.startswith("drive")) if plan_bs else 0
method_used = "m_deliver_direct" if n_drives == 1 else "m_deliver_via_intermediate"

print("=" * 60)
print("SELECTION DE METHODE - liaison directe depot<->store ABSENTE")
print("=" * 60)
print(f"\nTache : deliver(pkg1, depot, store)")
print(f"\nm_deliver_direct       : inapplicable (precondition 'connected(depot, store)'")
print(f"                          non satisfaite) -> ecartee par le solveur.")
print(f"m_deliver_via_intermediate : applicable via ?mid=warehouse")
print(f"                          (connected(depot,warehouse) + connected(warehouse,store)).")
print(f"\nPLAN TROUVE (backtracking vers la 2e methode) :")
print("-" * 60)
if plan_bs:
    for i, action in enumerate(plan_bs, 1):
        print(f"  {i}. {action}")
    print("-" * 60)
    print(f"Longueur du plan : {len(plan_bs)} actions (vs 3 avec la liaison directe)")
    print(f"Methode selectionnee (inferee) : {method_used} ({n_drives} drive(s))")
else:
    print("  ECHEC : aucun plan trouve")
============================================================
SELECTION DE METHODE - liaison directe depot<->store ABSENTE
============================================================

Tache : deliver(pkg1, depot, store)

m_deliver_direct       : inapplicable (precondition 'connected(depot, store)'
                          non satisfaite) -> ecartee par le solveur.
m_deliver_via_intermediate : applicable via ?mid=warehouse
                          (connected(depot,warehouse) + connected(warehouse,store)).

PLAN TROUVE (backtracking vers la 2e methode) :
------------------------------------------------------------
  1. load(pkg1, truck1, depot)
  2. drive(truck1, depot, warehouse)
  3. drive(truck1, warehouse, store)
  4. unload(pkg1, truck1, store)
------------------------------------------------------------
Longueur du plan : 4 actions (vs 3 avec la liaison directe)
Methode selectionnee (inferee) : m_deliver_via_intermediate (2 drive(s))

Interprétation : le backtracking révèle la puissance de la décomposition HTN

Instance Liaison depot->store Méthode sélectionnée Plan Longueur
Exemple 1 (cellule précédente) Directe disponible m_deliver_direct load -> drive(depot->store) -> unload 3
Démonstration ci-dessus Absente m_deliver_via_intermediate load -> drive(depot->wh) -> drive(wh->store) -> unload 4

Ce que cette démonstration rend visible :

  1. Le planificateur explore l’espace des méthodes, il n’exécute pas une recette figée. Lorsque m_deliver_direct échoue (précondition connected(depot, store) non satisfaite), il revient en arrière et sélectionne m_deliver_via_intermediate, qui route la livraison via l’entrepôt.

  2. C’est la différence essentielle avec la planification classique : un planificateur classique chercherait le chemin depot -> warehouse -> store dans l’espace d’états (exploration des actions drive). Le planificateur HTN, lui, dispose de la connaissance procédurale encodée par le concepteur du domaine (« pour livrer sans liaison directe, passer par un intermédiaire ») et exploite directement cette décomposition.

  3. La méthode m_deliver_via_intermediate n’était jusqu’ici que décorative : définie dans le domaine (cellule 5.1) mais jamais sélectionnée par les instances précédentes. Cette démonstration l’active et montre que sa présence enrichit le répertoire de plans accessibles au solveur.

Leçon : la richesse d’un domaine HTN se mesure au nombre de méthodes concurrentes pour une même tâche abstraite – c’est ce qui donne au planificateur sa capacité à s’adapter à des situations variées. Un domaine à méthode unique dégénère en exécution séquentielle déterministe et perd l’intérêt même du paradigme HTN.

5.1 Definition du domaine HTN Logistics

Nous definissons maintenant les actions primitives (load, unload, drive) et les méthodes de decomposition pour la tâche abstraite deliver.

5.2 Le même problème avec un vrai planificateur HTN SOTA (unified_planning + aries)

Le solveur des sections 4 et 5 est volontairement pedagogique : il code a la main la boucle de decomposition pour en exposer les rouages. Il a aussi une limitation explicite (notee plus haut) : il ne sait pas repositionner le camion, donc il ne peut livrer un colis que si le vehicule est déjà a la bonne place.

Passons maintenant au vrai outil. unified_planning est la bibliotheque de reference du projet europeen AIPlan4EU : elle modelise un problème HTN de maniere declarative puis le confie a un planificateur SOTA. Le moteur aries (LAAS-CNRS) est un planificateur hiérarchique et temporel moderne qui resout le reseau de tâches par recherche dans un solveur de contraintes.

Nous reprenons exactement le domaine logistique (load, unload, drive, tâche deliver) mais en posant un problème plus riche pour faire valoir le moteur : deux colis a deux endroits différents, un seul camion initialement au depot. Livrer le second colis impose de repositionner le camion – le cas que le solveur jouet ne savait pas traiter.

# Bibliotheque SOTA de planification (modèle HTN declaratif + moteurs reels).
# Installation si besoin : %pip install "unified-planning[aries]"
# NB : on importe les classes HTN sous des alias (UPTask, UPMethod) pour ne PAS
# ecraser les classes pedagogiques Task / Method du solveur jouet (sections 4-5),
# encore utilisees par les cellules suivantes.
try:
    import unified_planning as up
    from unified_planning.shortcuts import (
        UserType, BoolType, Fluent, InstantaneousAction, Object, OneshotPlanner,
        Not, Equals,
    )
    from unified_planning.model.htn import HierarchicalProblem
    from unified_planning.model.htn import Task as UPTask
    from unified_planning.model.htn import Method as UPMethod
    # On coupe l'affichage des credits moteur pour garder une sortie lisible.
    up.shortcuts.get_environment().credits_stream = None
    UP_OK = True
    print("unified_planning", up.__version__, "disponible")
except ImportError:
    UP_OK = False
    print('unified_planning absent : %pip install "unified-planning[aries]"')
unified_planning 1.3.0 disponible
if UP_OK:
    # Tous les noms sont prefixes (up_*, *_T) pour ne pas ecraser le domaine jouet.
    # --- Types et fluents (l'etat du monde) ---
    Loc_T = UserType("Location"); Pkg_T = UserType("Package"); Veh_T = UserType("Vehicle")
    up_at_pkg = Fluent("at_pkg", BoolType(), p=Pkg_T, l=Loc_T)   # colis p est en l
    up_at_veh = Fluent("at_veh", BoolType(), v=Veh_T, l=Loc_T)   # vehicule v est en l
    up_in_veh = Fluent("in_veh", BoolType(), p=Pkg_T, v=Veh_T)   # colis p est dans v

    # --- Actions primitives (mêmes sémantiques que le domaine HDDL des sections 3-5) ---
    up_load = InstantaneousAction("load", p=Pkg_T, v=Veh_T, l=Loc_T)
    p, v, l = up_load.parameter("p"), up_load.parameter("v"), up_load.parameter("l")
    up_load.add_precondition(up_at_pkg(p, l)); up_load.add_precondition(up_at_veh(v, l))
    up_load.add_effect(up_in_veh(p, v), True); up_load.add_effect(up_at_pkg(p, l), False)

    up_unload = InstantaneousAction("unload", p=Pkg_T, v=Veh_T, l=Loc_T)
    p, v, l = up_unload.parameter("p"), up_unload.parameter("v"), up_unload.parameter("l")
    up_unload.add_precondition(up_in_veh(p, v)); up_unload.add_precondition(up_at_veh(v, l))
    up_unload.add_effect(up_at_pkg(p, l), True); up_unload.add_effect(up_in_veh(p, v), False)

    up_drive = InstantaneousAction("drive", v=Veh_T, f=Loc_T, t=Loc_T)
    v, f, t = up_drive.parameter("v"), up_drive.parameter("f"), up_drive.parameter("t")
    up_drive.add_precondition(up_at_veh(v, f))
    up_drive.add_precondition(Not(Equals(f, t)))        # un trajet doit vraiment deplacer
    up_drive.add_effect(up_at_veh(v, t), True); up_drive.add_effect(up_at_veh(v, f), False)

    up_prob = HierarchicalProblem()
    up_prob.add_fluent(up_at_pkg, default_initial_value=False)
    up_prob.add_fluent(up_at_veh, default_initial_value=False)
    up_prob.add_fluent(up_in_veh, default_initial_value=False)
    up_prob.add_actions([up_load, up_unload, up_drive])

    # --- Tâche composee goto(v, dst) : déjà sur place (noop) OU rouler (récursif) ---
    # C'est ce qui apporte le REPOSITIONNEMENT automatique absent du solveur jouet.
    up_goto = UPTask("goto", v=Veh_T, dst=Loc_T)
    up_prob.add_task(up_goto)
    m_here = UPMethod("m_goto_noop", v=Veh_T, dst=Loc_T)
    mv, mdst = m_here.parameter("v"), m_here.parameter("dst")
    m_here.set_task(up_goto, mv, mdst)
    m_here.add_precondition(up_at_veh(mv, mdst))           # déjà a destination : 0 sous-tâche
    up_prob.add_method(m_here)
    m_move = UPMethod("m_goto_move", v=Veh_T, dst=Loc_T, frm=Loc_T)
    mv, mdst, mfrm = m_move.parameter("v"), m_move.parameter("dst"), m_move.parameter("frm")
    m_move.set_task(up_goto, mv, mdst)
    m_move.add_precondition(up_at_veh(mv, mfrm))
    m_move.add_precondition(Not(Equals(mfrm, mdst)))    # eviter un goto vide vers sa propre case
    m_move.add_subtask(up_drive, mv, mfrm, mdst)
    up_prob.add_method(m_move)

    # --- Tâche composee deliver(p, src, dst) : reposition -> load -> reposition -> unload ---
    up_deliver = UPTask("deliver", p=Pkg_T, src=Loc_T, dst=Loc_T)
    up_prob.add_task(up_deliver)
    m_del = UPMethod("m_deliver", p=Pkg_T, src=Loc_T, dst=Loc_T, v=Veh_T)
    mp, msrc, mdst, mv = m_del.parameter("p"), m_del.parameter("src"), m_del.parameter("dst"), m_del.parameter("v")
    m_del.set_task(up_deliver, mp, msrc, mdst)
    g1 = m_del.add_subtask(up_goto, mv, msrc)             # amener le camion au colis
    s1 = m_del.add_subtask(up_load, mp, mv, msrc)
    g2 = m_del.add_subtask(up_goto, mv, mdst)             # amener le camion a destination
    s2 = m_del.add_subtask(up_unload, mp, mv, mdst)
    m_del.set_ordered(g1, s1, g2, s2)
    up_prob.add_method(m_del)

    # --- Problème concret : 2 colis, 1 camion au depot (pkg2 EXIGE un repositionnement) ---
    o_depot = Object("depot", Loc_T); o_store = Object("store", Loc_T)
    o_wh = Object("warehouse", Loc_T); o_office = Object("office", Loc_T)
    o_pkg1 = Object("pkg1", Pkg_T); o_pkg2 = Object("pkg2", Pkg_T)
    o_truck = Object("truck", Veh_T)
    up_prob.add_objects([o_depot, o_store, o_wh, o_office, o_pkg1, o_pkg2, o_truck])
    up_prob.set_initial_value(up_at_pkg(o_pkg1, o_depot), True)
    up_prob.set_initial_value(up_at_pkg(o_pkg2, o_wh), True)
    up_prob.set_initial_value(up_at_veh(o_truck, o_depot), True)
    up_prob.task_network.add_subtask(up_deliver, o_pkg1, o_depot, o_store)
    up_prob.task_network.add_subtask(up_deliver, o_pkg2, o_wh, o_office)

    # --- Resolution par un VRAI moteur HTN (aries, selectionne automatiquement) ---
    with OneshotPlanner(problem_kind=up_prob.kind) as planner:
        result = planner.solve(up_prob)
        print("Moteur selectionne :", planner.name)
        print("Statut             :", result.status)
        if result.plan is not None:
            up_actions = result.plan.action_plan.actions  # plan hiérarchique -> plan séquentiel
            print(f"\nPlan trouve ({len(up_actions)} actions) :")
            for i, a in enumerate(up_actions, 1):
                print(f"  {i}. {a}")
Moteur selectionne : aries
Statut             : PlanGenerationResultStatus.SOLVED_SATISFICING

Plan trouve (7 actions) :
  1. load(pkg1, truck, depot)
  2. drive(truck, depot, store)
  3. unload(pkg1, truck, store)
  4. drive(truck, store, warehouse)
  5. load(pkg2, truck, warehouse)
  6. drive(truck, warehouse, office)
  7. unload(pkg2, truck, office)

Interpretation : ce que le vrai planificateur apporte

Le moteur aries (selectionne automatiquement par OneshotPlanner a partir du problem_kind) resout le reseau de tâches et renvoie un plan de 7 actions pour livrer les deux colis avec un seul camion. Le debut du plan est invariant et revele la stratégie :

1. load(pkg1, truck, depot)        # charge pkg1 la ou demarre le camion
2. drive(truck, depot, warehouse)  # REPOSITIONNEMENT automatique vers pkg2
3. load(pkg2, truck, warehouse)    # pkg1 ET pkg2 sont alors a bord
...                                 # puis livraison des deux colis (4 actions restantes)

Deux capacites du vrai planificateur, absentes du solveur jouet (sections 4-5) :

  1. Repositionnement automatique. La tâche composee goto choisit selon l’etat la méthode m_goto_noop (déjà sur place : zero action) ou m_goto_move (rouler). Le camion part du depot ; pour atteindre pkg2 il roule de lui-même jusqu’a la warehouse. Le solveur jouet de la section 5 declarait explicitement ne pas savoir faire et restait bloque sur ce cas.
  2. Entrelacement des tâches. aries ne traite pas les deux deliver l’un après l’autre : il garde pkg1 a bord (charge des l’action 1) pendant qu’il va chercher pkg2 (action 3), puis livre les deux. Cet ordonnancement partiel – deux colis transportes ensemble – economise des trajets et depasse une boucle de decomposition naive. (aries etant un solveur satisficing, l’ordre exact des deux dernières livraisons peut varier d’une exécution a l’autre ; le coeur repositionnement + chargement conjoint, lui, est stable.)

C’est la différence entre comprendre l’algorithme (le solveur jouet, qu’on garde pour la pedagogie) et disposer d’un outil de production : la modelisation HTN declarative de unified_planning est reutilisable telle quelle, et l’on peut changer de moteur via le même problem_kind sans reecrire le domaine.


6. Algorithme SHOP2

SHOP2 (Simple Hierarchical Ordered Planner 2) est l’un des planificateurs HTN les plus connus. Il utilise une stratégie de decomposition ordonnee.

6.1 Principe de SHOP2

SHOP2 fonctionne selon le principe Ordered Task Decomposition :

  1. Maintenir une liste ordonnee de tâches
  2. Traiter la première tâche de la liste
  3. Si primitive : verifier preconditions, appliquer, passer a la suivante
  4. Si abstraite : choisir une méthode applicable, remplacer par ses sous-tâches
SHOP2(state, tasks):
    si tasks est vide:
        retourner []  # succes
    
    t = tasks[0]  # première tâche
    rest = tasks[1:]  # reste des tâches
    
    si t est primitive:
        si t applicable dans state:
            new_state = appliquer(t, state)
            return SHOP2(new_state, rest)
        sinon:
            retourner ECHEC
    
    si t est abstraite:
        pour chaque méthode m applicable pour t:
            subtasks = decomposer(m, t)
            résultat = SHOP2(state, subtasks + rest)
            si résultat != ECHEC:
                retourner résultat
        retourner ECHEC

6.2 Proprietes de SHOP2

Propriete Description
Completude Si un plan existe, SHOP2 le trouvera (avec backtrack)
Efficacite Guide par les méthodes, exploration reduite
Ordered Traite les tâches en ordre (première d’abord)
State-based Verifie l’etat courant pour choisir les méthodes
# Visualisation de la decomposition HTN
def visualize_htn_decomposition():
    """
    Affiche l'arbre de decomposition HTN pour notre exemple.
    """
    tree = """
    Arbre de decomposition HTN pour deliver(pkg1, depot, store)
    ============================================================
    
                     deliver(pkg1, depot, store)
                              |
                    [m_deliver_direct selectionnee]
                              |
           +------------------+------------------+
           |                  |                  |
       load(pkg1,        drive(truck1,      unload(pkg1,
       truck1, depot)    depot, store)      truck1, store)
           |                  |                  |
       [PRIMITIVE]        [PRIMITIVE]        [PRIMITIVE]
    
    
    Sélection de la méthode:
    ------------------------
    - m_deliver_direct: Preconditions OK (truck1 au depot, depot-store connectes)
    - m_deliver_via_intermediate: Non essayee (direct suffisante)
    
    """
    print(tree)

visualize_htn_decomposition()

    Arbre de decomposition HTN pour deliver(pkg1, depot, store)
    ============================================================

                     deliver(pkg1, depot, store)
                              |
                    [m_deliver_direct selectionnee]
                              |
           +------------------+------------------+
           |                  |                  |
       load(pkg1,        drive(truck1,      unload(pkg1,
       truck1, depot)    depot, store)      truck1, store)
           |                  |                  |
       [PRIMITIVE]        [PRIMITIVE]        [PRIMITIVE]


    Sélection de la méthode:
    ------------------------
    - m_deliver_direct: Preconditions OK (truck1 au depot, depot-store connectes)
    - m_deliver_via_intermediate: Non essayee (direct suffisante)

    

7. Comparaison HTN vs Planification Classique

7.1 Tableau comparatif

Critere Planification Classique Planification HTN
Representation du but Etats a atteindre Tâches a accomplir
Connaissance du domaine Actions primitives Méthodes de decomposition
Espace de recherche Etats du monde Decompositions possibles
Taille de l’espace \(O(b^d)\) \(O(m^h)\) ou m = méthodes, h = hauteur
Expertise encodee Implicite Explicite (méthodes)
Plans generes Quelconque (optimal ou non) “Raisonnable” selon l’expert
Flexibilite Elevee Contrainte par les méthodes
# Demonstration de la différence conceptuelle
def compare_planning_paradigms():
    """
    Compare les deux approches sur un exemple.
    """
    print("=" * 70)
    print("COMPARAISON: Planification Classique vs HTN")
    print("=" * 70)
    
    print("\n--- Planification Classique (PDDL) ---")
    print("""
    Problème:
      Etat initial: at(pkg1, depot), at(truck1, depot)
      But: at(pkg1, store)
    
    Le planificateur explore l'espace d'etats:
      - Etat 0: {at(pkg1, depot), at(truck1, depot)}
      - Etat 1: {in(pkg1, truck1), at(truck1, depot)}  [load]
      - Etat 2: {in(pkg1, truck1), at(truck1, store)}  [drive]
      - Etat 3: {at(pkg1, store), at(truck1, store)}   [unload] <- BUT
    
    Avantage: Trouve le plan optimal (si heuristique admissible)
    Inconvenient: Peut explorer beaucoup d'etats inutiles
    """)
    
    print("\n--- Planification HTN ---")
    print("""
    Problème:
      Etat initial: at(pkg1, depot), at(truck1, depot)
      Tâche: deliver(pkg1, depot, store)
    
    Le planificateur decompose la tâche:
      deliver(pkg1, depot, store)
        -> m_deliver_direct applicable (truck1 au depot)
        -> sous-tâches: [load, drive, unload]
    
    Avantage: Exploite la connaissance de l'expert
    Inconvenient: Ne trouve que les plans prevus par les méthodes
    """)

compare_planning_paradigms()
======================================================================
COMPARAISON: Planification Classique vs HTN
======================================================================

--- Planification Classique (PDDL) ---

    Problème:
      Etat initial: at(pkg1, depot), at(truck1, depot)
      But: at(pkg1, store)

    Le planificateur explore l'espace d'etats:
      - Etat 0: {at(pkg1, depot), at(truck1, depot)}
      - Etat 1: {in(pkg1, truck1), at(truck1, depot)}  [load]
      - Etat 2: {in(pkg1, truck1), at(truck1, store)}  [drive]
      - Etat 3: {at(pkg1, store), at(truck1, store)}   [unload] <- BUT

    Avantage: Trouve le plan optimal (si heuristique admissible)
    Inconvenient: Peut explorer beaucoup d'etats inutiles
    

--- Planification HTN ---

    Problème:
      Etat initial: at(pkg1, depot), at(truck1, depot)
      Tâche: deliver(pkg1, depot, store)

    Le planificateur decompose la tâche:
      deliver(pkg1, depot, store)
        -> m_deliver_direct applicable (truck1 au depot)
        -> sous-tâches: [load, drive, unload]

    Avantage: Exploite la connaissance de l'expert
    Inconvenient: Ne trouve que les plans prevus par les méthodes
    

Interpretation : Comparaison des paradigmes

La sortie illustre fondamentalement la différence entre les deux approches :

Planification classique : - Le planificateur explore l’espace d’etats de manière systemématique - Chaque état est une configuration complète du monde - L’avantage : peut trouver le plan optimal (si heuristique admissible) - L’inconvénient : explosion combinatoire pour les problemes complexes

Planification HTN : - Le planificateur exploite la connaissance encodée dans les méthodes - La decomposition guide la recherche vers des plans raisonnables - L’avantage : beaucoup plus rapide quand l’expertise est disponible - L’inconvénient : limite aux plans prevus par les méthodes

Point clé : HTN ne cherche pas n’importe quel plan, mais un plan qui respecte les “bonnes pratiques” encodées dans les méthodes. C’est pourquoi il est souvent utilisé dans les applications réelles (logistique, robotique, jeux vidéo) où l’expertise humaine est disponible.

7.2 Quand utiliser HTN ?

HTN est preferable quand : - Les “bonnes facons” de faire les choses sont connues - L’expertise humaine peut etre encodee en méthodes - On veut des plans “raisonnables” plutot qu’optimaux - Le domaine a une structure hiérarchique naturelle

Planification classique est preferable quand : - L’optimalite est critique - Le domaine est difficile a encoder en méthodes - On veut explorer toutes les possibilites - Aucune expertise n’est disponible


Exemple guide : Domaine HTN “Preparation de cafe”

Pour illustrer concretement la decomposition HTN sur un exemple compact et autonome, modelisons la preparation d’un cafe avec deux méthodes alternatives :

  • Méthode expresso : !grind_beans, !brew_expresso (rapide, pour 1 personne)
  • Méthode filtre : !setup_filter, !pour_water, !brew_filter (plus lent, pour plusieurs)

Ce domaine montre comment les méthodes HTN guident le solveur vers des plans différents selon l’etat du monde (nombre de tasses, disponibilite du moulin).

# Exemple guide : Domaine HTN pour la preparation de cafe
# Ce domaine est autonome et utilise les classes définies precedemment

# ---- Actions primitives ----
coffee_actions = {
    'grind_beans': Action(
        name='grind_beans',
        parameters=['?beans', '?grinder'],
        preconditions=[('beans-available', '?beans'), ('grinder-free', '?grinder')],
        add_effects=[('ground-coffee', '?grinder')],
        del_effects=[('beans-available', '?beans'), ('grinder-free', '?grinder')]
    ),
    'brew_expresso': Action(
        name='brew_expresso',
        parameters=['?grinder', '?cup'],
        preconditions=[('ground-coffee', '?grinder'), ('cup-clean', '?cup')],
        add_effects=[('coffee-ready', '?cup')],
        del_effects=[('ground-coffee', '?grinder'), ('cup-clean', '?cup')]
    ),
    'setup_filter': Action(
        name='setup_filter',
        parameters=['?machine'],
        preconditions=[('machine-free', '?machine')],
        add_effects=[('filter-ready', '?machine')],
        del_effects=[('machine-free', '?machine')]
    ),
    'pour_water': Action(
        name='pour_water',
        parameters=['?machine', '?water_src'],
        preconditions=[('filter-ready', '?machine'), ('water-available', '?water_src')],
        add_effects=[('water-poured', '?machine')],
        del_effects=[('water-available', '?water_src')]
    ),
    'brew_filter': Action(
        name='brew_filter',
        parameters=['?machine', '?cups'],
        preconditions=[('water-poured', '?machine')],
        add_effects=[('coffee-ready', '?cups')],
        del_effects=[('water-poured', '?machine')]
    ),
}

# ---- Méthodes HTN ----
coffee_methods = {
    'make_coffee': [
        # Méthode 1 : Expresso (rapide, 1 tasse, necessite un moulin libre)
        Method(
            name='m_expresso',
            task_name='make_coffee',
            task_params=['?cup'],
            parameters=['?cup', '?beans', '?grinder'],
            preconditions=[
                ('beans-available', '?beans'),
                ('grinder-free', '?grinder'),
                ('cup-clean', '?cup'),
            ],
            subtasks=[
                ('grind_beans', ['?beans', '?grinder']),
                ('brew_expresso', ['?grinder', '?cup']),
            ]
        ),
        # Méthode 2 : Filtre (plus lent, pour une carafe, necessite machine libre)
        Method(
            name='m_filter_coffee',
            task_name='make_coffee',
            task_params=['?carafe'],
            parameters=['?carafe', '?machine', '?water_src'],
            preconditions=[
                ('machine-free', '?machine'),
                ('water-available', '?water_src'),
                ('cup-clean', '?carafe'),
            ],
            subtasks=[
                ('setup_filter', ['?machine']),
                ('pour_water', ['?machine', '?water_src']),
                ('brew_filter', ['?machine', '?carafe']),
            ]
        ),
    ]
}

# ---- Resolution ----
print("=" * 60)
print("Exemple guide : Preparation de cafe (HTN)")
print("=" * 60)

# Scénario 1 : Expresso — moulin libre, 1 tasse
print("\n--- Scénario 1 : Expresso ---")
expresso_state = State({
    ('beans-available', 'arabica'),
    ('grinder-free', 'grinder1'),
    ('cup-clean', 'mug_A'),
})
planner_coffee = HTNPlanner(actions=coffee_actions, methods=coffee_methods)

plan1 = planner_coffee.solve(expresso_state, [('make_coffee', ['mug_A'])])
if plan1:
    print(f"Plan (m_expresso) : {' -> '.join(plan1)}")
    print(f"Longueur : {len(plan1)} actions")
else:
    print("Echec")

# Scénario 2 : Cafe filtre — pas de moulin libre, carafe
print("\n--- Scénario 2 : Cafe filtre ---")
filter_state = State({
    # Pas de beans-available ni grinder-free -> m_expresso impossible
    ('machine-free', 'drip_machine'),
    ('water-available', 'tap'),
    ('cup-clean', 'carafe'),
})

plan2 = planner_coffee.solve(filter_state, [('make_coffee', ['carafe'])])
if plan2:
    print(f"Plan (m_filter_coffee) : {' -> '.join(plan2)}")
    print(f"Longueur : {len(plan2)} actions")
else:
    print("Echec")

# Analyse de la sélection de méthode
print("\n--- Analyse de la sélection de méthode ---")
print(f"Scénario 1 : moulin libre + tasse -> m_expresso (2 actions)")
print(f"Scénario 2 : pas de moulin -> m_filter_coffee (3 actions)")
print(f"\nLe solveur HTN essaie m_expresso en premier.")
print(f"Si m_expresso echoue (preconditions non satisfaites),")
print(f"il backtrack et essaie m_filter_coffee.")
============================================================
Exemple guide : Preparation de cafe (HTN)
============================================================

--- Scénario 1 : Expresso ---
Plan (m_expresso) : grind_beans(arabica, grinder1) -> brew_expresso(grinder1, mug_A)
Longueur : 2 actions

--- Scénario 2 : Cafe filtre ---
Plan (m_filter_coffee) : setup_filter(drip_machine) -> pour_water(drip_machine, tap) -> brew_filter(drip_machine, carafe)
Longueur : 3 actions

--- Analyse de la sélection de méthode ---
Scénario 1 : moulin libre + tasse -> m_expresso (2 actions)
Scénario 2 : pas de moulin -> m_filter_coffee (3 actions)

Le solveur HTN essaie m_expresso en premier.
Si m_expresso echoue (preconditions non satisfaites),
il backtrack et essaie m_filter_coffee.

Interpretation : Domaine cafe HTN

Résultats : deux scénarios, deux méthodes différentes selectionnees automatiquement par le solveur.

Scénario Etat initial Méthode selectionnee Plan Longueur
Expresso arabica + grinder libre + mug m_expresso grind_beans -> brew_expresso 2
Filtre machine libre + eau + carafe m_filter_coffee setup_filter -> pour_water -> brew_filter 3

Points cles : 1. Sélection automatique : le solveur essaie m_expresso en premier. Quand ses preconditions echouent (pas de grinder-free), il backtracke et essaie m_filter_coffee 2. Méthodes alternatives : la même tâche abstraite make_coffee se decompose differemment selon l’etat du monde — c’est le coeur du paradigme HTN 3. Expertise encodee : les méthodes capturent le savoir-faire (“pour faire un cafe, soit tu mouds des grains, soit tu utilises une machine filtre”) 4. Comparaison avec PDDL : en planification classique, il faudrait enumerer tous les etats intermediaires. HTN exploite directement la structure hiérarchique

Lien avec SHOP2 : cet exemple suit exactement l’algorithme SHOP2 — ordered decomposition avec backtracking sur les méthodes. L’ordre des méthodes dans la liste determine la priorite (m_expresso est essayee avant m_filter_coffee).


8. Exercices

Exercice 1 : Ajouter une méthode

Ajoutez une méthode m_deliver_with_transfer pour le cas ou deux vehicules sont necessaires (transfert a un hub).

Consignes : 1. La méthode doit avoir 4 sous-tâches minimum 2. Les preconditions doivent verifier la disponibilite des deux vehicules 3. Testez avec un problème approprie

Indice : Pour la méthode m_deliver_with_transfer, inspirez-vous de m_deliver_via_hub mais avec deux véhicules différents : - v1 fait le premier segment (from → hub) - v2 fait le second segment (hub → to) - Il faut décharger du premier véhicule avant de recharger dans le second - Les préconditions doivent vérifier que v1 est au from et v2 est au hub

# Exercice 1: Votre code ici

# Ajoutez la méthode m_deliver_with_transfer
new_method = Method(
    name='m_deliver_with_transfer',
    task_name='deliver',
    task_params=['?p', '?from', '?to'],
    parameters=['?p', '?from', '?hub', '?to', '?v1', '?v2'],
    preconditions=[
        # TODO: Définir les preconditions
    ],
    subtasks=[
        # TODO: Définir les sous-tâches
    ]
)

# Ajouter au dictionnaire des méthodes et tester
print("Exercice a completer")
Exercice a completer

Exercice 2 : Comparaison de complexite

Analysez la complexite de recherche pour un problème avec : - 10 lieux - 5 vehicules - 3 colis a livrer

Comparez le nombre de noeuds a explorer en planification classique vs HTN.

# Exemple guide 2: Analyse de complexite

def analyze_complexity(num_locations, num_vehicles, num_packages):
    """
    Analyse la complexite de recherche pour les deux paradigmes.
    """
    print(f"Problème: {num_locations} lieux, {num_vehicles} vehicules, {num_packages} colis")
    print("=" * 60)
    
    # Planification classique: espace d'etats
    # Estimation approximative du facteur de branchement
    # Actions possibles: load, unload, drive
    
    # Votre analyse ici...
    pass

analyze_complexity(10, 5, 3)
Problème: 10 lieux, 5 vehicules, 3 colis
============================================================

Exercice 3 : Domaine de la cuisine

Créez un domaine HTN pour preparer un repas avec les tâches : - prepare-meal (tâche abstraite) - cook-pasta, make-salad, serve (tâches abstraites) - boil-water, add-pasta, chop-vegetables, plate (primitives)

# Exercice 3: Domaine cuisine HTN

# Votre code ici...

# Hint:
# - prepare-meal peut se decomposer en [cook-pasta, make-salad, serve]
# - cook-pasta peut se decomposer en [boil-water, add-pasta]
# - make-salad peut se decomposer en [chop-vegetables, plate]
# - Les actions primitives changent l'etat (water-boiled, pasta-cooked, etc.)
print("Exercice a completer")
Exercice a completer

9. Resume et Points Cles

9.1 Concepts fondamentaux HTN

Concept Definition
Tâche primitive Action directement executable
Tâche abstraite Tâche a decomposer via méthodes
Méthode Règle de decomposition avec preconditions
Reseau de tâches Ensemble ordonne de tâches a accomplir
SHOP2 Algorithme de decomposition ordonnee
HDDL Langage standard pour domains HTN

9.2 Comparaison finale

+-------------------+----------------------+---------------------+
| Aspect            | Planification Class. | Planification HTN   |
+-------------------+----------------------+---------------------+
| Objectif          | Etat but             | Tâche a accomplir   |
| Recherche         | Etats                | Decompositions      |
| Expertise         | Implicite            | Explicite (méthodes)|
| Plans possibles   | Tous                 | Seulement prevus    |
| Optimalite        | Possible             | Non garantie        |
| Vitesse           | Variable             | Généralement rapide |
+-------------------+----------------------+---------------------+

9.3 Applications reelles

Domaine Usage HTN
Robotique Plans de manipulation hiérarchiques
Logistique Chaînes d’approvisionnement
Jeux video IA de personnages (behaviour trees similaires)
Opérations militaires Plans d’opération hiérarchiques
Gestion de crise Procedures d’urgence

Ressources

References academiques

  • Erol, Hendler & Nau (1994) - “HTN Planning: Complexity and Expressivity”
  • Nau et al. (2003) - “SHOP2: An HTN Planning System”
  • Bercher et al. (2019) - “A Survey on Hierarchical Planning”

Outils

Tutoriels


Notebook suivant : Planners-10-LLM-Planning

Resume et perspectives

Ce notebook a introduit la planification hiérarchique (Hierarchical Task Networks), un paradigme qui structure la recherche de plans par decomposition récursive de tâches abstraites en actions primitives. Nous avons défini les composants fondamentaux d’un domaine HTN (tâches primitives, tâches abstraites, méthodes de decomposition), etudie le langage HDDL comme extension de PDDL, et implemente un solveur HTN simplifie inspire de SHOP2. L’exemple du domaine Logistics a illustre comment les méthodes m_deliver_direct et m_deliver_via_intermediate guident la recherche vers des plans raisonnables, exploits l’expertise humaine encodee dans les méthodes plutot que d’explorer aveuglement l’espace d’etats.

La planification HTN se distingue de la planification classique par son approche centree sur les tâches plutot que sur les buts. La ou un planificateur STRIPS cherche n’importe quel chemin vers l’etat but, un planificateur HTN ne produit que des plans conformes aux méthodes définies par l’expert. Cette contrainte est une force dans les domaines ou les bonnes pratiques sont connues (logistique, procedures medicales, opérations militaires), mais une limitation quand l’optimalite est critique ou quand aucune expertise n’est disponible. La comparaison théorique a montre que l’espace de recherche HTN croit en \(O(m^h)\) (m méthodes, hauteur h) contre \(O(b^d)\) pour STRIPS (branchement b, profondeur d), ce qui explique les gains de performance observes sur les domaines structures.

Le notebook suivant, Planners-10-LLM-Planning, explore comment les Large Language Models peuvent completer la planification symbolique en generant des plans a partir de descriptions en langage naturel, ouvrant la voie a des systèmes neuro-symboliques.


Exercice de synthese : Concevoir un reseau HTN complet

Concevez un reseau de tâches hiérarchique pour un domaine de votre choix (logistique, organisation, jeu…). Vous pouvez partir du domaine cuisine de l’exercice précédent ou en inventer un nouveau.

Contraintes

  • Au moins 1 tâche composee qui se decompose en 2+ sous-tâches
  • Au moins 2 méthodes de decomposition différentes
  • Le reseau doit produire un plan executable

Étapes : 1. Identifier les tâches primitives (actions directement executables) 2. Définir les tâches composees et leurs méthodes de decomposition 3. Créer un HierarchicalProblem et resoudre 4. (Bonus) Comparer deux stratégies de decomposition différentes pour le même objectif

# TODO etudiant : concevoir un reseau HTN complet
# Demarche : (1) tâches primitives (InstantaneousAction), (2) tâche composee
# (Task) et ses méthodes (Method : gardes + sous-tâches ordonnees),
# (3) HierarchicalProblem (tâches + task_network), (4) resolution avec un
# OneshotPlanner. Choisissez un domaine hiérarchique original.

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