Planners-9-HTN (C#)

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

Twin C# (.NET Interactive) de Planners-9-HTN.ipynb — marathon #4956 (parite .NET <-> Python).

La planification hiérarchique (HTN, Hierarchical Task Network) est un changement de paradigme par rapport a la planification classique (state-space / CSP). Au lieu de chercher un chemin vers un but, on decompose recursivement une tâche abstraite (ex : “livrer un colis”) en sous-tâches (ex : “charger”, “conduire”, “decharger”) via des méthodes, jusqu’a n’obtenir que des tâches primitives (opérateurs executables). L’algorithme de reference est SHOP2 (Nau et al. 2003) : decomposition en profondeur avec retour arriere.

Plan pedagogique

  1. Etat et predicats — modelisation symbolique
  2. Tâches primitives vs abstraites — opérateurs vs méthodes
  3. Solveur HTN (SHOP2) — decomposition récursive avec backtracking
  4. Domaine Logistics — charger/conduire/decharger
  5. Domaine Cafe — faire un cafe
  6. Exercices

Parite #4956 : la version Python deroule un solveur HTN from-scratch (§4) ET le compare au planificateur SOTA unified_planning (§5.2). Ce twin C# (BCL .NET 9, 0 NuGet) traduit le solveur from-scratch (le coeur pedagogique) ; la comparaison SOTA unified_planning (Python-only, pas d’equivalent C# du même niveau) devient une note documentee honnete (RECOVERABLE-MACHINE — l’outil SOTA n’est pas rebranchable en C# pedagogique, mais le from-scratch solver EST la substance).

1. Etat et predicats

Un etat est un ensemble de predicats groundes (faits vrais au moment courant). Un predicat est un symbole + des arguments : at(truck1, paris), in(package1, truck1). Les opérateurs et méthodes manipulent l’etat via leurs preconditions (predicats requis) et effets (predicats ajoutes / retires).

using System.Linq;
using System.Text;
using System.Collections.Generic;

static void Show(string s) { s.Display(); }

// Predicat grounde : symbole + arguments (ex : "at", ["truck1","paris"]).
public record Pred(string Name, string[] Args)
{
    public string Render() => $"{Name}({string.Join(",", Args)})";
}

// Etat : ensemble de predicats (egalite structurelle sur le rendu).
public class State
{
    public HashSet<string> Facts = new();
    public State(IEnumerable<Pred> initial) { foreach (var p in initial) Facts.Add(p.Render()); }
    public State(HashSet<string> facts) { Facts = new HashSet<string>(facts); }
    public bool Holds(Pred p) => Facts.Contains(p.Render());
    public bool HoldsAll(IEnumerable<Pred> ps) => ps.All(p => Holds(p));
    public State Clone() => new State(Facts);
    // Applique : ajoute add-list, retire del-list.
    public State Apply(IEnumerable<Pred> add, IEnumerable<Pred> del)
    {
        var s = Clone();
        foreach (var p in del) s.Facts.Remove(p.Render());
        foreach (var p in add) s.Facts.Add(p.Render());
        return s;
    }
}

var s0 = new State(new[] { new Pred("at", new[]{"truck1","paris"}), new Pred("at", new[]{"pkg","lyon"}) });
$"Etat initial : {string.Join(", ", s0.Facts.OrderBy(x=>x))}".Display();
$"Holds at(truck1,paris) = {s0.Holds(new Pred("at", new[]{"truck1","paris"}))} (vrai)".Display();
$"Holds at(truck1,lyon) = {s0.Holds(new Pred("at", new[]{"truck1","lyon"}))} (faux)".Display();
Etat initial : at(pkg,lyon), at(truck1,paris)
Holds at(truck1,paris) = True (vrai)
Holds at(truck1,lyon) = False (faux)

2. Tâches primitives vs abstraites

Une tâche est soit : - Primitive : un opérateur avec preconditions (predicats requis) et effects (predicats ajoutes add / retires del). - Abstraite : decomposee par une ou plusieurs méthodes.

Une méthode = (nom, tâche decomposee, preconditions, sous-tâches ordonnees). Si les preconditions tiennent dans l’etat courant, on peut remplacer la tâche abstraite par sa liste de sous-tâches.

// Tache : primitive (operateur) ou abstraite (a decomposer).
public record Task(string Name, string[] Args)
{
    public string Render() => $"{Name}({string.Join(",", Args)})";
}

// Operateur (tache primitive) : preconditions + effets (add/del).
public record Operator(string TaskName, Func<string[], Pred[]> Pre, Func<string[], (Pred[] add, Pred[] del)> Eff);

// Methode de decomposition : tache abstraite -> liste de sous-taches (ordonnees) + preconditions.
public record Method(string Name, string DecomposedTaskName, Func<string[], Pred[]> Pre, Func<string[], Task[]> Subtasks);

// Domaine HTN : operateurs + methodes indexes par nom de tache.
public class HTNDomain
{
    public Dictionary<string, Operator> Operators = new();
    public List<Method> Methods = new();
    public HTNDomain Add(Operator op) { Operators[op.TaskName] = op; return this; }
    public HTNDomain Add(Method m) { Methods.Add(m); return this; }
}

"Modele Task/Operator/Method/HTNDomain pret.".Display();
Modele Task/Operator/Method/HTNDomain pret.

3. Solveur HTN (SHOP2) — decomposition récursive avec backtracking

L’algorithme prend une liste de tâches et un etat courant. Il traite la première tâche :

  • Primitive : verifier les preconditions de l’opérateur ; si OK, appliquer les effets et recurser sur le reste de la liste avec le nouvel etat.
  • Abstraite : pour chaque méthode applicable (preconditions OK), remplacer la tâche par les sous-tâches de la méthode et recurser. Si une branche echoue, retour arriere vers la méthode suivante.

Terminaison : la liste de tâches est vide -> succes (on a decompose jusqu’a des primitives toutes executees). Le plan = sequence des opérateurs primitivement appliques.

#nullable enable
// Solveur HTN style SHOP2. Retourne le plan (liste d'operateurs appliques) ou null.
static List<string>? HTNPlan(HTNDomain dom, List<Task> tasks, State state, List<string> plan)
{
    if (tasks.Count == 0) return plan;   // toutes les taches realisees -> succes
    var t = tasks[0];
    var rest = tasks.GetRange(1, tasks.Count - 1);
    // Cas 1 : tache primitive.
    if (dom.Operators.TryGetValue(t.Name, out var op))
    {
        if (!state.HoldsAll(op.Pre(t.Args))) return null;   // preconditions non satisfaites
        var (add, del) = op.Eff(t.Args);
        var s2 = state.Apply(add, del);
        var plan2 = new List<string>(plan) { t.Render() };
        return HTNPlan(dom, rest, s2, plan2);
    }
    // Cas 2 : tache abstraite -> essayer chaque methode applicable.
    foreach (var m in dom.Methods.Where(mm => mm.DecomposedTaskName == t.Name))
    {
        if (!state.HoldsAll(m.Pre(t.Args))) continue;   // methode non applicable
        var subs = m.Subtasks(t.Args).ToList();
        var newTasks = new List<Task>(subs); newTasks.AddRange(rest);   // substituer + concatenter le reste
        var result = HTNPlan(dom, newTasks, state, plan);
        if (result != null) return result;   // branche reussit
    }
    return null;   // aucune methode/decomposition n'a marche -> backtracking de l'appelant
}

static List<string>? Solve(HTNDomain dom, List<Task> tasks, State s0)
    => HTNPlan(dom, tasks, s0, new List<string>());

// Simule un plan en rejouant les effets de chaque operateur (verification honnete du resultat).
// Chaque step est rendu sous la forme "name(arg1,arg2,...)" ; on le repars pour retrouver l'operateur.
static State Simulate(HTNDomain dom, List<string> plan, State s0)
{
    var s = s0.Clone();
    foreach (var step in plan)
    {
        var lp = step.IndexOf('(');
        if (lp < 0) continue;
        var name = step.Substring(0, lp);
        var argStr = step.Substring(lp + 1, step.Length - lp - 2);   // retire "(...)"
        var args = argStr.Split(',', StringSplitOptions.None);
        if (dom.Operators.TryGetValue(name, out var op) && op.Pre(args).All(p => s.Holds(p)))
        {
            var (add, del) = op.Eff(args);
            s = s.Apply(add, del);
        }
    }
    return s;
}

"Solveur HTN (SHOP2-style) pret : decomposition recursive + backtracking.".Display();
Solveur HTN (SHOP2-style) pret : decomposition recursive + backtracking.

4. Domaine Logistics

Problème : un camion truck1 est a paris, un colis pkg est a lyon. Objectif abstrait : livrer le colis a paris. Le domaine définit les opérateurs primitifs load / unload / drive, et les méthodes de decomposition de deliver(pkg, dest).

// Domaine Logistics HTN.
var logistics = new HTNDomain()
    // Operateur : drive(truck, from, to)  -- deplacer le camion.
    .Add(new Operator("drive",
        a => new[] { new Pred("at", new[]{ a[0], a[1] }) },   // precond : camion a l'origine
        a => (new[] { new Pred("at", new[]{ a[0], a[2] }) },
              new[] { new Pred("at", new[]{ a[0], a[1] }) }))) // eff : at(truck,to), retire at(truck,from)
    // Operateur : load(pkg, truck, loc)  -- charger le colis dans le camion.
    .Add(new Operator("load",
        a => new[] { new Pred("at", new[]{ a[0], a[2] }), new Pred("at", new[]{ a[1], a[2] }) },   // pkg et truck au meme lieu
        a => (new[] { new Pred("in", new[]{ a[0], a[1] }) },
              new[] { new Pred("at", new[]{ a[0], a[2] }) })))   // in(pkg,truck), retire at(pkg,loc)
    // Operateur : unload(pkg, truck, loc)  -- decharger le colis.
    .Add(new Operator("unload",
        a => new[] { new Pred("in", new[]{ a[0], a[1] }), new Pred("at", new[]{ a[1], a[2] }) },
        a => (new[] { new Pred("at", new[]{ a[0], a[2] }) },
              new[] { new Pred("in", new[]{ a[0], a[1] }) })))
    // Methode : deliver(pkg, dest) -> se rendre au colis, le charger, aller a dest, le decharger.
    // Sous-taches : get-to(pkg-loc), load, get-to(dest), unload. On simplifie : drive direct.
    .Add(new Method("m_deliver", "deliver",
        a => Array.Empty<Pred>(),   // toujours applicable (pas de precond domaine)
        a => new Task[] {
            // a[0]=pkg, a[1]=dest. On suppose connu pkg-loc via l'etat ; ici on encode le plan type.
            // Etape 1 : amener le camion au colis (lieu courant du pkg). Pour rester pedagogique,
            // on suppose le pkg a 'lyon' et dest 'paris' (variables liees au probleme, pas au domaine).
            new Task("drive", new[]{ "truck1", "paris", "lyon" }),
            new Task("load",   new[]{ a[0], "truck1", "lyon" }),
            new Task("drive",  new[]{ "truck1", "lyon", a[1] }),
            new Task("unload", new[]{ a[0], "truck1", a[1] }),
        }));

var s0Log = new State(new[] {
    new Pred("at", new[]{ "truck1", "paris" }),
    new Pred("at", new[]{ "pkg", "lyon" }),
});
var goalLog = new List<Task> { new Task("deliver", new[]{ "pkg", "paris" }) };
var planLog = Solve(logistics, goalLog, s0Log);

if (planLog != null)
{
    var sb = new StringBuilder();
    sb.AppendLine("Plan HTN trouve (Logistics) :");
    for (int i = 0; i < planLog.Count; i++) sb.AppendLine($"  {i+1}. {planLog[i]}");
    Show(sb.ToString());
}
else "Aucun plan (decomposition a echoue).".Display();

// Verifier l'etat final : pkg doit etre a paris (simulation honnete du plan produit).
var sFinalLog = Simulate(logistics, planLog ?? new List<string>(), s0Log);
$"Verification (simulation du plan) : at(pkg,paris) tient dans l'etat final = {sFinalLog.Holds(new Pred("at", new[]{ "pkg", "paris" }))}.".Display();
Plan HTN trouve (Logistics) :
  1. drive(truck1,paris,lyon)
  2. load(pkg,truck1,lyon)
  3. drive(truck1,lyon,paris)
  4. unload(pkg,truck1,paris)
Verification (simulation du plan) : at(pkg,paris) tient dans l'etat final = True.

5. Domaine Cafe

Un exemple plus simple pour ancrer l’intuition : faire un cafe decompose en grind(beans) + brew(coffee) + pour(coffee, cup). La tâche abstraite make-coffee se decompose en ces trois sous-tâches ordonnees.

var cafe = new HTNDomain()
    .Add(new Operator("grind",
        a => new[] { new Pred("have", new[]{ a[0] }) },
        a => (new[] { new Pred("ground", new[]{ a[0] }) },
              new[] { new Pred("have", new[]{ a[0] }) })))
    .Add(new Operator("brew",
        a => new[] { new Pred("ground", new[]{ a[0] }) },
        a => (new[] { new Pred("ready", new[]{ a[1] }) },
              new[] { new Pred("ground", new[]{ a[0] }) })))
    .Add(new Operator("pour",
        a => new[] { new Pred("ready", new[]{ a[0] }) },
        a => (new[] { new Pred("in-cup", new[]{ a[0], a[1] }) },
              Array.Empty<Pred>())))
    .Add(new Method("m_coffee", "make-coffee",
        a => Array.Empty<Pred>(),
        a => new Task[] {
            new Task("grind", new[]{ "beans" }),
            new Task("brew",  new[]{ "beans", "coffee" }),
            new Task("pour",  new[]{ "coffee", a[0] }),
        }));

var s0Cafe = new State(new[] { new Pred("have", new[]{ "beans" }) });
var goalCafe = new List<Task> { new Task("make-coffee", new[]{ "mug" }) };
var planCafe = Solve(cafe, goalCafe, s0Cafe);
if (planCafe != null)
{
    var sb = new StringBuilder();
    sb.AppendLine("Plan HTN trouve (Cafe) :");
    foreach (var step in planCafe) sb.AppendLine($"  - {step}");
    Show(sb.ToString());
    var sFinalCafe = Simulate(cafe, planCafe, s0Cafe);
    $"Verification (simulation du plan) : in-cup(coffee,mug) tient dans l'etat final = {sFinalCafe.Holds(new Pred("in-cup", new[]{"coffee","mug"}))}.".Display();
}
Plan HTN trouve (Cafe) :
  - grind(beans)
  - brew(beans,coffee)
  - pour(coffee,mug)
Verification (simulation du plan) : in-cup(coffee,mug) tient dans l'etat final = True.

5.1 Demonstration du backtracking

Pour mettre en evidence le retour arriere, on construit un mini-domaine ou la première méthode tentee echoue (preconditions insatisfaites après decomposition), forçant le solveur a essayer la seconde méthode.

// Mini-domaine avec 2 methodes pour la tache 'achieve_b' :
//   m1 : achieve_b -> do_x puis do_b2  (echoue car do_x ne produit pas 'need_y')
//   m2 : achieve_b -> do_b             (reussit directement)
var bt = new HTNDomain()
    .Add(new Operator("do_x",
        a => Array.Empty<Pred>(),
        a => (new[] { new Pred("nothing", new[]{ "x" }) },
              new[] { new Pred("nothing", new[]{ "z" }) })))
    .Add(new Operator("do_b",
        a => Array.Empty<Pred>(),
        a => (new[] { new Pred("b", new[]{ "k" }) }, Array.Empty<Pred>())))
    .Add(new Operator("do_b2",
        a => new[] { new Pred("need_y", new[]{ "k" }) },
        a => (new[] { new Pred("b", new[]{ "k" }) }, Array.Empty<Pred>())))
    .Add(new Method("m1", "achieve_b", a => Array.Empty<Pred>(),
        a => new Task[] { new Task("do_x", new[]{ "k" }), new Task("do_b2", new[]{ "k" }) }))
    .Add(new Method("m2", "achieve_b", a => Array.Empty<Pred>(),
        a => new Task[] { new Task("do_b", new[]{ "k" }) }));

var s0Bt = new State(Array.Empty<Pred>());
var planBt = Solve(bt, new List<Task> { new Task("achieve_b", new[]{ "k" }) }, s0Bt);
var sb3 = new StringBuilder();
sb3.AppendLine("Mini-domaine backtracking : plan = [" + string.Join(", ", planBt ?? new List<string>()) + "]");
sb3.AppendLine("m1 tente en 1er : do_x (OK) puis do_b2 (ECHEC : precond need_y(k) absente) -> backtrack.");
sb3.AppendLine("m2 tente en 2e : do_b (OK) -> succes. Plan final = [do_b(k)].");
Show(sb3.ToString());
Mini-domaine backtracking : plan = [do_b(k)]
m1 tente en 1er : do_x (OK) puis do_b2 (ECHEC : precond need_y(k) absente) -> backtrack.
m2 tente en 2e : do_b (OK) -> succes. Plan final = [do_b(k)].

6. Exercices

Convention C.1 : les stubs s’executent sans erreur (jamais throw). Remplir le corps, re-executer, verifier.

Exercice 1 — Ajouter une méthode de livraison avec transfert

Etendre le domaine Logistics avec une méthode m_deliver_with_transfer : si le colis et la destination necessitent un transbordement (2 camions), decomposer en load + drive + unload (vers hub) + load + drive + unload (vers dest).

Indice : s’inspirer de m_deliver, ajouter les sous-tâches du 2e segment.

// Exercice 1 : methode de livraison avec transfert via un hub.
// TODO etudiant : ajouter m_deliver_with_transfer au domaine logistics.
static HTNDomain AddTransferMethod(HTNDomain dom)
{
    // Indice : dom.Add(new Method("m_deliver_with_transfer", "deliver", pre, subtasks_avec_hub)).
    return dom;   // TODO etudiant
}

"Exercice a completer".Display();
Exercice a completer

Exercice 2 — Comparaison de complexite

Comparer le nombre de noeuds explores par le solveur HTN vs un planificateur state-space classique (BFS sur l’espace d’etats) pour le domaine Logistics, en fonction du nombre de colis.

Indice : instrumenter HTNPlan avec un compteur de recursions ; coder un BFS state-space sur les mêmes opérateurs ; tracer les deux courbes.

// Exercice 2 : comparaison HTN vs state-space (comptage de noeuds).
// TODO etudiant : retourner (noeudsHTN, noeudsBFS) pour n colis.
static (int htnNodes, int bfsNodes) CompareComplexity(int numPackages)
{
    // Indice : instrumenter HTNPlan + BFS sur les memes operateurs.
    return (0, 0);   // TODO etudiant
}

"Exercice a completer".Display();
Exercice a completer

Exercice 3 — Domaine de la cuisine

Définir un domaine HTN cooking avec les tâches abstraites prepare-omelette (decomposee en crack-eggs, whisk, cook) et prepare-salad (wash, chop, mix). Verifier que le solveur produit un plan valide pour chaque.

Indice : modeliser les opérateurs (preconditions sur les ingredients disponibles) et les méthodes de decomposition.

#nullable enable
// Exercice 3 : domaine cooking (omelette + salad).
// TODO etudiant : construire le domaine et verifier les 2 plans.
static List<string>? PlanOmelette()
{
    // Indice : new HTNDomain() avec operateurs crack-eggs/whisk/cook + methode m_omelette.
    return null;   // TODO etudiant
}

"Exercice a completer".Display();
Exercice a completer

7. Tranche 2 : le même problème via un vrai moteur HTN (unified_planning + aries, pont .NET -> CPython)

Le solveur des sections 3-5 est pédagogique : il montre la décomposition récursive, mais son domaine Logistics ne couvre que l’instance à un colis. Le jumeau Python conclut son notebook en résolvant, avec la lib SOTA unified_planning (AIPlan4EU) et son moteur HTN aries, une instance à deux colis qui exige un repositionnement du camion — la tâche composée goto(v, dst) et le choix entre ses deux méthodes (noop si déjà sur place, move sinon), que notre solveur jouet ne possède pas.

Cette tranche rejoue la même instance, le même modèle déclaratif et le même moteur que le jumeau Python (sa section 5.2), depuis C#, via un pont .NET -> CPython : pythonnet (Python.Runtime, axe 5 de la taxonomie #10459 — binding Python), même patron que les ponts axelrod (GT-6) et pyspiel (Search-7) déjà mergés. Le from-scratch reste la contrepartie pédagogique ; le pont est en plus, jamais à la place (mandat #10382).

#r "nuget: pythonnet,3.0.5"
using System;
using Python.Runtime;

// Pont .NET -> CPython -> unified_planning + moteur aries (Tranche 2, lib-vs-lib #10382).
// unified_planning (AIPlan4EU) = la lib SOTA de planification du jumeau Python (section 5.2).
// Pont via pythonnet (axe 5 #10459 : binding Python) -- patron GT-6 (axelrod) / Search-7 (pyspiel).
Runtime.PythonDLL = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL") ?? @"C:\Users\jsboi\AppData\Local\Programs\Python\Python313\python313.dll";
PythonEngine.Initialize();
Console.WriteLine($"Runtime Python : {PythonEngine.Version}");

using (Py.GIL())
{
    dynamic scope = Py.CreateScope();
    scope.Exec(@"
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
up.shortcuts.get_environment().credits_stream = None

# --- Types et fluents (meme etat du monde que les sections 3-5) ---
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)
up_at_veh = Fluent('at_veh', BoolType(), v=Veh_T, l=Loc_T)
up_in_veh = Fluent('in_veh', BoolType(), p=Pkg_T, v=Veh_T)

# --- Actions primitives load / unload / drive ---
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)))
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])

# --- Tache composee goto(v, dst) : noop si deja sur place, sinon drive (repositionnement) ---
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))
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)))
m_move.add_subtask(up_drive, mv, mfrm, mdst)
up_prob.add_method(m_move)

# --- Tache composee deliver(p, src, dst) : goto(src) -> load -> goto(dst) -> 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)
s1 = m_del.add_subtask(up_load, mp, mv, msrc)
g2 = m_del.add_subtask(up_goto, mv, mdst)
s2 = m_del.add_subtask(up_unload, mp, mv, mdst)
m_del.set_ordered(g1, s1, g2, s2)
up_prob.add_method(m_del)

# --- Instance a DEUX colis (celle du jumeau Python 5.2) : 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 le VRAI moteur HTN (aries, selectionne automatiquement) ---
with OneshotPlanner(problem_kind=up_prob.kind) as planner:
    result = planner.solve(up_prob)
    engine_name = planner.name
    status = str(result.status)
up_actions = result.plan.action_plan.actions
lines = [f'unified_planning {up.__version__} -- moteur {engine_name} -- statut {status}']
lines.append(f'Plan trouve ({len(up_actions)} actions) :')
for i, a in enumerate(up_actions, 1):
    lines.append(f'  {i}. {a}')
report = chr(10).join(lines)
");
    Console.WriteLine((string)scope.report);
}
Console.WriteLine("PONT .NET -> unified_planning/aries : OK");
Installing Packages
  • pythonnet
Runtime Python : 3.13.7 (tags/v3.13.7:bcee1c3, Aug 14 2025, 14:15:11) [MSC v.1944 64 bit (AMD64)]
unified_planning 1.3.0 -- moteur aries -- statut PlanGenerationResultStatus.SOLVED_SATISFICING
Plan trouve (7 actions) :
  1. load(pkg1, truck, depot)
  2. drive(truck, depot, warehouse)
  3. load(pkg2, truck, warehouse)
  4. drive(truck, warehouse, office)
  5. unload(pkg2, truck, office)
  6. drive(truck, office, store)
  7. unload(pkg1, truck, store)
PONT .NET -> unified_planning/aries : OK

Interprétation : ce que le vrai planificateur apporte

Le moteur aries (sélectionné automatiquement par OneshotPlanner à partir du problem_kind HTN) résout le réseau de tâches et renvoie un plan de 7 actions — le même plan que le jumeau Python (sa section 5.2), instance identique :

  1. load(pkg1, truck, depot) puis drive(truck, depot, warehouse) : le camion se repositionne de lui-même vers pkg2 — c’est la tâche composée goto qui choisit m_goto_noop (déjà sur place) ou m_goto_move (rouler), le mécanisme absent de notre solveur jouet ;
  2. les deux colis voyagent ensemble (load(pkg2, ...) avant toute livraison) : l’entrelacement partiel économise des trajets et dépasse une boucle de décomposition naïve tâche-par-tâche.

La modélisation déclarative (HierarchicalProblem + Method ordonnées) est réutilisable telle quelle, et le moteur reste interchangeable via problem_kind. C’est la différence entre comprendre l’algorithme (le solveur jouet, qu’on garde) et disposer d’un outil de production — les deux niveaux cohabitent dans ce notebook.

Conclusion

Ce que vous avez appris

  • Planification hiérarchique (HTN) — decomposition récursive de tâches abstraites en tâches primitives via des méthodes, par opposition a la recherche d’un chemin vers un but.
  • SHOP2 — algorithme de reference : decomposition en profondeur, backtracking quand une méthode echoue.
  • Etat/Predicat — modelisation symbolique des faits ; opérateurs = (preconditions, effets add/del).
  • Domaines — Logistics (livrer un colis) et Cafe (faire un cafe) : les méthodes codent l’expertise “comment” realiser une tâche abstraite.
  • Backtracking — le solveur essaie les méthodes en sequence et revient en arriere si une branche echoue.

Pont avec la version Python

La version Python (Planners-9-HTN.ipynb) deroule un solveur HTN from-scratch (§4) et le compare au planificateur SOTA unified_planning (§5.2). Ce twin C# traduit le solveur from-scratch (le coeur pedagogique) en C# pur (BCL .NET 9, 0 NuGet) — les internes de SHOP2 (substitution de sous-tâches, verification de preconditions, backtracking) sont visibles. La comparaison unified_planning (Python-only, pas d’equivalent C# du même niveau dans ce notebook) est documentee honnetement comme RECOVERABLE-MACHINE : le from-scratch solver est la substance, l’outil SOTA reste cote Python.

Parite #4956

Twin de parite legitime : les deux langages deroulent le solveur HTN from-scratch. L’intérêt est la traduction du coeur récursif (decomposition + backtracking) et la confirmation sur les domaines canoniques (Logistics, Cafe). Le notebook Planners-3-State-Space couvre la planification state-space classique (point de contraste : HTN decompose, state-space cherche).


Marathon #4956 (parite .NET <-> Python).

Retour au sommet