Navigation : Index | << OR-Tools C# | HTN C# >> Ce notebook est le jumeau C# (.NET Interactive) du notebook Python Planners-8-Temporal.ipynb. Il réimplémente from-scratch (BCL .NET, 0 NuGet, 0 unified_planning, 0 ortools) les concepts de la planification temporelle : PDDL 2.1 actions duratives, algèbre d’intervalles d’Allen, réseau temporel simple (STN) et ordonnancement avec contraintes de précédence et ressources.
Objectifs pédagogiques
Modéliser une action durative PDDL 2.1 (conditions at start, over all, at end, effets).
Manipuler l’algèbre d’intervalles d’Allen (13 relations binaires entre intervalles temporels).
Construire et vérifier un STN (Simple Temporal Network) par Floyd-Warshall sur la matrice des distances.
Détecter les conflits temporels (exclusion mutuelle, chevauchement interdit).
Ordonnancer des tâches avec précédence + ressources capacitaires (variante RCPSP), produire un diagramme de Gantt ASCII.
Pourquoi un twin from-scratch ?
Le notebook Python original s’appuie sur unified_planning (API Python de planification) et ortools CP-SAT (Google) pour exprimer et résoudre le problème temporel. Ces libs ne sont pas mobilisables nativement côté .NET Interactive sans NuGet lourds. Le twin C# reconstruit les concepts — Allen, STN, Floyd-Warshall, scheduling — pour les rendre transparents et exécutables sur toute machine .NET 9+. Le value-add (EPIC #4956 Prong B) est pédagogique : chaque mécanisme (composition d’Allen, propagation de contraintes STN, heuristique de scheduling) est visible dans le code C#.
Prérequis
.NET 9.0+ (kernel csharp). Familiarité avec la planification classique state-space (cf. Planners-3-State-Space-Csharp.ipynb). Aucune librairie externe.
Cadre pedagogique et prerequisites
Ce notebook jumeau C# de Planners-8-Temporal.ipynb (Python) couvre la planification temporelle, sous-domaine de la planification automatique ou le temps est une dimension explicite (les actions durent, les fenetres temporelles existent, les conflits d’allocation sont possibles). Quatre piliers sont construits from-scratch en C# avant d’etre compares a un moteur SOTA de production (CP-SAT).
Pourquoi ce notebook maintenant ? La planification temporelle est un probleme fondamental de la recherche operationnelle (ordonnancement de chaines de production, planification de personnel, logistique de livraison). Elle est aussi un terrain fertile pour les outils modernes : solveurs CP-SAT, MIP, metaheuristiques. Comprendre les fondations (from-scratch) permet de lire les sorties des solveurs avec un oeil critique.
Prerequisites :
C# 9+ (memes bases que Planners-1 : record, HashSet<T>, LINQ). Les specificites enum, readonly struct, sealed class sont documentees inline.
Notions de planification classique STRIPS (voir Planners-1, Planners-2, Planners-3 si besoin).
Sensibilite aux contraintes combinatoires : un probleme a 4 taches et 2 ressources a 4! = 24 ordres possibles ; un probleme a 10 taches et 3 ressources a 10! = 3.6 millions. Un solveur automatique devient vite indispensable.
Plan du notebook : 5 parties theoriques (PDDL 2.1, Allen, STN, conflits, RCPSP) + 1 Tranche 2 pratique (CP-SAT via NuGet) + 3 exercices. Chaque partie peut etre lue independamment ; les imports sont groupes dans la cellule Setup qui suit.
Note d’integration : la Tranche 2 utilise le package NuGet Google.OrTools via .NET Interactive (#r "nuget: ..."). Premier tel appel dans la serie Planners C# – le notebook documente pas-a-pas la sortie du dotnet-interactive.
Setup — utilitaires d’affichage
using System;using System.Collections.Generic;using System.Linq;using System.Text;staticvoidShow(object o){try{display(o);}catch{ Console.WriteLine(o);}}staticvoidShow(string label,object o)=>Show($"{label}: {o}");Show("Setup","OK — BCL .NET, 0 NuGet (pas de unified_planning / ortools).");
Setup: OK — BCL .NET, 0 NuGet (pas de unified_planning / ortools).
Partie 1 — PDDL 2.1 : actions duratives
En planification classique (STRIPS/PDDL 1), une action est instantanée. PDDL 2.1 (Fox & Long, 2003) introduit les actions duratives : une action s’étend sur un intervalle \([t_{start}, t_{end}]\) de durée \(\delta\), avec des conditions et effets qualifiés temporellement :
at start : condition/effect au début de l’action,
over all : condition maintenue pendant toute la durée,
at end : condition/effect à la fin.
Cela permet la concurrence : deux actions duratives peuvent se chevaucher si elles ne se violent pas mutuellement (exclusion mutuelle temporelle sur une ressource, par exemple).
Allen (1983) définit 13 relations binaires entre deux intervalles temporels \(I_1 = [s_1, e_1]\) et \(I_2 = [s_2, e_2]\) (avec \(s_i < e_i\)) : before, after, meets, met-by, overlaps, overlapped-by, during, contains, starts, started-by, finishes, finished-by, equals.
Elles forment une algèbre de relation avec une table de composition (si \(R_1(I_1,I_2)\) et \(R_2(I_2,I_3)\) alors \(R_1 \circ R_2 \subseteq \{\text{relations possibles}\}(I_1,I_3)\)). On implémente la classification par comparaison des bornes, puis la table de composition pour la propagation de contraintes.
Allen: Matrice des relations d'Allen :
I1 I2 I3 I4
I1 equal meets overlaps contains
I2 met-by equal overlapped-by after
I3 overlapped-by overlaps equal overlapped-by
I4 during before overlaps equal
Inverse de meets: met-by
Partie 3 — Réseau temporel simple (STN) et Floyd-Warshall
Un Simple Temporal Network (Dechter, Meiri & Pearl, 1991) est un graphe dont les arêtes contraignent la distance temporelle entre deux événements (timepoints). Une arête \(X \xrightarrow{[a,b]} Y\) impose \(a \leq t_Y - t_X \leq b\).
On encode chaque contrainte \([a,b]\) par deux arêtes orientées dans la matrice des distances\(D\) : - \(D[X,Y] \leq b\) (borne supérieure), - \(D[Y,X] \leq -a\) (borne inférieure, retournée).
Le STN est consistant ssi aucun cycle négatif n’apparaît après l’exécution de Floyd-Warshall (all-pairs shortest path). Un cycle négatif \(\Rightarrow\) contradiction temporelle insatisfiable.
// --- STN + Floyd-Warshall from-scratch ---publicsealedclass STN{public List<string> Nodes {get;}=new();publicdouble[,] Dist;// matrice des distancesprivateint n => Nodes.Count;publicSTN(IEnumerable<string> nodes){ Nodes = nodes.ToList();int N = Nodes.Count; Dist =newdouble[N, N];for(int i =0; i < N; i++)for(int j =0; j < N; j++) Dist[i, j]= i == j ?0:double.PositiveInfinity;}publicintIdx(string node)=> Nodes.IndexOf(node);// Contrainte a <= tY - tX <= bpublicvoidAddConstraint(string x,string y,double a,double b){int X =Idx(x), Y =Idx(y); Dist[X, Y]= Math.Min(Dist[X, Y], b); Dist[Y, X]= Math.Min(Dist[Y, X],-a);}// Floyd-Warshall : retourne true si consistant (pas de cycle negatif).public(bool Consistent,double[,] D)FloydWarshall(){int N = n;var D =(double[,])Dist.Clone();for(int k =0; k < N; k++)for(int i =0; i < N; i++)for(int j =0; j < N; j++)if(D[i, k]+ D[k, j]< D[i, j]) D[i, j]= D[i, k]+ D[k, j];bool consistent =true;for(int i =0; i < N; i++)if(D[i, i]<0) consistent =false;return(consistent, D);}publicstringDump(){var sb =newStringBuilder(); sb.AppendLine($"STN ({n} nodes): {string.Join(",", Nodes)}");return sb.ToString();}}// --- STN consistant : 3 evenements A, B, C ---var stn1 =newSTN(new[]{"X0","A","B","C"});stn1.AddConstraint("X0","A",1,5);// A se produit entre t=1 et t=5stn1.AddConstraint("X0","B",2,8);// B entre t=2 et t=8stn1.AddConstraint("A","B",1,10);// B apres A, au moins 1 unite plus tardstn1.AddConstraint("B","C",0,4);// C apres B, dans les 4 unitesstn1.AddConstraint("X0","C",0,15);// C avant t=15var(ok1, D1)= stn1.FloydWarshall();Show("STN 1", stn1.Dump());Show("STN 1 consistant ?", ok1);Show("Bornes X0->C", $"[{-D1[stn1.Idx("C"), stn1.Idx("X0")]}, {D1[stn1.Idx("X0"), stn1.Idx("C")]}]");// --- STN INCONSISTANT : A avant B, B avant A (cycle negatif) ---var stn2 =newSTN(new[]{"A","B"});stn2.AddConstraint("A","B",5,10);// B apres A d'au moins 5stn2.AddConstraint("B","A",5,10);// A apres B d'au moins 5 => contradictionvar(ok2, D2)= stn2.FloydWarshall();Show("STN 2 (contradictoire)", stn2.Dump());Show("STN 2 consistant ?", ok2);Show("Diagonale (cycle negatif ?)", D2[0,0]<0? $"OUI, D[A,A]={D2[0,0]} (contradiction)":"non");
Partie 4 — Conflits temporels et exclusion mutuelle
Deux actions duratives sont en exclusion mutuelle temporelle si elles ne peuvent pas se chevaucher (par exemple, deux actions utilisant exclusivement la même ressource). On détecte un conflit en testant si leurs intervalles planifiés sont en relation overlaps / during / contains / etc. (tout sauf before/after/meets/met-by/equal disjoint).
// --- Detection de conflits temporels ---publicstaticclass TemporalMutex{// Deux intervalles sont-ils en conflit (chevauchement) ?publicstaticboolConflicts(Interval a, Interval b){var rel = AllenAlgebra.Classify(a, b);// Conflit si les intervalles se chevauchent (pas strictement ordonnes).return rel != AllenRel.Before&& rel != AllenRel.After&& rel != AllenRel.Meets&& rel != AllenRel.MetBy;}// Verifie qu'un schedule (liste d'intervalles nommes) n'a pas de conflit 2-a-2.publicstatic List<(Interval, Interval)>FindConflicts(List<Interval> schedule){var conflicts =new List<(Interval, Interval)>();for(int i =0; i < schedule.Count; i++)for(int j = i +1; j < schedule.Count; j++)if(Conflicts(schedule[i], schedule[j])) conflicts.Add((schedule[i], schedule[j]));return conflicts;}}// --- Schedule sans conflit ---var sched1 =new List<Interval>{newInterval("weld",0,4),newInterval("paint",4,7),// meets weld, pas de conflitnewInterval("inspect",8,10),// apres, pas de conflit};var conflicts1 = TemporalMutex.FindConflicts(sched1);Show("Schedule 1 conflits", conflicts1.Count==0?"AUCUN (OK)":string.Join("; ", conflicts1.Select(c => $"{c.Item1.Name}<->{c.Item2.Name}")));// --- Schedule avec conflit (meme machine) ---var sched2 =new List<Interval>{newInterval("weld_A",0,5),newInterval("weld_B",3,8),// overlaps weld_A => conflit (1 machine)};var conflicts2 = TemporalMutex.FindConflicts(sched2);Show("Schedule 2 conflits", conflicts2.Count==0?"AUCUN":string.Join("; ", conflicts2.Select(c => $"{c.Item1.Name}<->{c.Item2.Name} [{c.Item1}] vs [{c.Item2}]")));
Schedule 1 conflits: AUCUN (OK)
Schedule 2 conflits: weld_A<->weld_B [weld_A[0,5]] vs [weld_B[3,8]]
Partie 5 — Ordonnancement (RCPSP-lite) et diagramme de Gantt
On assemble les briques précédentes : étant donné un ensemble de tâches (durée, précédences, demande en ressource) et une capacité de ressource, on calcule un schedule par heuristique gloutonne (liste de priorité + placement au plus tôt sous contraintes de précédence et de capacité). On produit ensuite un diagramme de Gantt ASCII.
Le scénario ci-dessous est construit avec contention de ressource — plus de tâches disponibles à un instant donné que la capacité n’en permet d’exécuter en parallèle. C’est précisément la situation qui justifie le scheduler : sans contrainte de capacité bindante, un simple placement au plus tôt (aveugle aux ressources) donnerait le même schedule, et la logique de vérification de capacité du scheduler ne servirait à rien.
C’est une variante simplifiée du RCPSP (Resource-Constrained Project Scheduling Problem, NP-difficile). L’heuristique gloutonne ne garantit pas l’optimalité (makespan minimal) mais produit un schedule réalisable en temps polynomial — la comparaison avec l’optimum known d’un benchmark (ex: PSPLIB) est laissée en exercice.
// --- Ordonnancement glouton RCPSP-lite + Gantt ASCII ---publicsealedclass Task{publicstring Name {get;}publicdouble Duration {get;}publicdouble Demand {get;}// demande en ressourcepublic List<string> Predecessors {get;}=new();publicdouble Start {get;set;}=-1;// -1 = non planifieepublicdouble Finish => Start + Duration;publicTask(string name,double dur,double demand,paramsstring[] preds){ Name = name; Duration = dur; Demand = demand; Predecessors = preds.ToList();}}publicsealedclass Scheduler{publicdouble Capacity {get;}publicScheduler(double cap){ Capacity = cap;}// Heuristique gloutonne : a chaque pas, planifier la tache disponible// (predecesseurs finis) dont le placement au plus tot respecte la capacite.// Les evenements temporels = fins de taches deja planifiees (grille discretee).publicvoidScheduleAll(List<Task> tasks){var done =new HashSet<string>();double horizon = tasks.Sum(t => t.Duration)+1;// Grille de temps finevar grid = Enumerable.Range(0,(int)Math.Ceiling(horizon)+1).Select(i =>(double)i).ToList();while(done.Count< tasks.Count){// Taches disponiblesvar ready = tasks.Where(t =>!done.Contains(t.Name)&& t.Predecessors.All(p => done.Contains(p))).ToList();if(ready.Count==0){/* deadlock cyclique */break;}// Par priorite : duree la plus longue d'abord (LPT heuristic) ready = ready.OrderByDescending(t => t.Duration).ToList();bool placed =false;foreach(var t in ready){// Trouver le plus tot start >= max(fins predecesseurs) tel que// pendant [start, start+dur] la capacite est respectee a tout instant.double earliest = t.Predecessors.Count==0?0: t.Predecessors.Max(p => tasks.First(x => x.Name== p).Finish);for(double s = earliest; s <= horizon; s +=1.0){bool ok =true;for(double tt = s; tt < s + t.Duration; tt +=1.0){double used = tasks.Where(x => x.Start>=0&& tt >= x.Start&& tt < x.Start+ x.Duration).Sum(x => x.Demand);if(used + t.Demand> Capacity +1e-9){ ok =false;break;}}if(ok){ t.Start= s; done.Add(t.Name); placed =true;break;}}if(placed)break;if(!placed && t.Start<0){/* impossible ce tour */}}if(!placed)break;// aucun placement possible => arreter (evite boucle infinie)}}// Diagramme de Gantt ASCII : lignes = taches, colonnes = unites de temps.publicstringGantt(List<Task> tasks){double makespan = tasks.Max(t => t.Finish);int W =(int)Math.Ceiling(makespan)+1;var sb =newStringBuilder(); sb.AppendLine($"Gantt (makespan = {makespan}, capacite = {Capacity}):"); sb.Append("task".PadRight(12)+" |");for(int t =0; t < W; t++) sb.Append((t %10).ToString()); sb.AppendLine(); sb.AppendLine(newstring('-',14+ W));foreach(var t in tasks){ sb.Append(t.Name.PadRight(12)+" |");var row =newstring('.', W).ToCharArray();for(int k =0; k < W; k++)if(k >= t.Start&& k < t.Start+ t.Duration) row[k]='#'; sb.AppendLine(newstring(row));}// Usage de capacite par unite de temps sb.Append("load".PadRight(12)+" |");for(int k =0; k < W; k++){double used = tasks.Where(x => x.Start>=0&& k >= x.Start&& k < x.Start+ x.Duration).Sum(x => x.Demand); sb.Append(used ==0?".": Math.Floor(used).ToString());} sb.AppendLine();return sb.ToString();}}// --- Scenario : atelier avec 2 machines (capacite = 2) ---// Prong B (#3801) : 3 decoupes (A, B, C) sont disponibles des t=0 et demandent 1 machine// chacune => demande initiale 3 > capacite 2. Le scheduler DOIT differer une tache : c'est// la qu'il justifie son existence (resoudre la contention de ressource), la ou un schedule// aveugle aux ressources placerait les 3 en parallele et violerait la capacite.var tasks =new List<Task>{newTask("A",3,1),// decoupe A (3h, 1 machine)newTask("B",3,1),// decoupe BnewTask("C",3,1),// decoupe C -> 3e tache en concurrence a t=0newTask("D",2,1,"A"),// soudure apres AnewTask("E",4,2,"B","C"),// assemblage apres B et C, demande les 2 machines};var sched =newScheduler(2);sched.ScheduleAll(tasks);Show("Schedule atelier (contention de ressource)", sched.Gantt(tasks));// --- Diagnostics : la contrainte de ressource est-elle active ? ---double makespanRC = tasks.Max(t => t.Finish);double demandAt0 = tasks.Where(t => t.Predecessors.Count==0).Sum(t => t.Demand);// Charge maximale par unite de temps (boucle explicite, evite le lambda imbrique CS9236)double peakLoad =0;for(int kk =0; kk <=(int)Math.Ceiling(makespanRC); kk++){double loadK = tasks.Where(x => x.Start>=0&& kk >= x.Start&& kk < x.Start+ x.Duration).Sum(x => x.Demand);if(loadK > peakLoad) peakLoad = loadK;}// Borne inferieure precedence : plus longue chaine (chemin critique), ressources ignorees.doubleCritLB(string name){var tk = tasks.First(x => x.Name== name);return tk.Predecessors.Count==0? tk.Duration: tk.Duration+ tk.Predecessors.Max(CritLB);}double lbRC = tasks.Max(t =>CritLB(t.Name));Show("Demande a t=0 (sans predecesseur)", $"{demandAt0} > capacite {sched.Capacity} -> schedule aveugle = INFEASIBLE");Show("Charge maximale observee", $"{peakLoad} = capacite {sched.Capacity} (contrainte SATUREE)");Show("Chemin critique (borne inferieure)", $"{lbRC}");Show("Makespan glouton", $"{makespanRC}");Show("Surcout resource (makespan - chemin critique)", $"{makespanRC - lbRC}");Show("Realisable ?", tasks.All(t => t.Start>=0)?"OUI (toutes planifiees)":"NON (certaines non placees)");
Schedule atelier (contention de ressource): Gantt (makespan = 10, capacite = 2):
task |01234567890
-------------------------
A |###........
B |###........
C |...###.....
D |...##......
E |......####.
load |2222212222.
Lecture du schedule — la contrainte de ressource est bindante
Le scheduler glouton produit ici un makespan de 10, alors que le chemin critique (la plus longue chaîne de précédence, en ignorant les ressources) ne vaut que 7. Cet écart de 3 unités est entièrement dû à la contention de ressource : c’est la trace, dans la sortie, du travail distinctif du scheduler.
Trois signaux le confirment dans la sortie ci-dessus :
Demande à t=0 = 3 > capacité 2. Les trois découpes A, B, C sont disponibles immédiatement et demandent chacune une machine. Un schedule aveugle aux ressources (placement au plus tôt sans vérifier la capacité) les placerait toutes en parallèle avec une charge 3 > 2 : il serait infeasible. Le scheduler doit donc déférer l’une d’elles.
Charge maximale observée = 2 = capacité. La contrainte est saturée : le test used + demand > capacity a effectivement rejeté des placements (sinon la charge resterait strictement inférieure à la capacité).
Le Gantt montre les reports. C démarre à \(t=3\) (et non \(t=0\)) parce que A + B saturent déjà les deux machines ; E démarre à \(t=6\) (et non \(t=3\)) parce qu’il attend la fin de C et exige les deux machines simultanément.
C’est exactement la situation que le scheduler RCPSP est censé résoudre — niveler la demande sous la capacité en différant des tâches — et qui serait invisible sur une instance sans contention (makespan égal au chemin critique, boucle de capacité jamais rejetante). L’heuristique LPT (Longest Processing Time first) fixe l’ordre de priorité ; elle n’est pas optimale (RCPSP est NP-difficile) — l’exercice 3 propose de comparer au makespan optimal obtenu par énumération des ordres topologiques.
Transition : du from-scratch au moteur de production
Les Sections 1 à 5 ont assemblé les briques d’un solveur from-scratch : modèle STRIPS duratif (Partie 1), algèbre d’intervalles d’Allen (Partie 2), réseau temporel simple + Floyd-Warshall (Partie 3), détection de conflits (Partie 4), ordonnancement glouton RCPSP-lite (Partie 5). Cette approche est nécessaire à la compréhension : vous savez maintenant ce que fait un solveur temporel et comment chaque pièce s’articule.
Mais le glouton de la Partie 5 a une limite de fond : il ne prouve pas l’optimalité du schedule qu’il produit. Sur l’instance de l’atelier (5 tâches A–E, capacité 2), il atteint ici le meilleur makespan possible — mais il ne le sait pas : aucun certificat n’accompagne le plan. La seule garantie théorique classique du glouton de liste LPT — au plus \(4/3 - 1/(3m)\) fois l’optimum (Graham 1969) — vaut pour P||Cmax (tâches indépendantes sur machines identiques) et ne se transfère pas au RCPSP démontré ici (précédences + demandes variables) : dans ce cadre, le glouton ne porte aucune borne d’approximation propre, et l’écart à l’optimum n’est pas contrôlé.
La Tranche 2 introduit un moteur SOTA : Google.OrTools CP-SAT (le solveur de programmation par contraintes de Google, branche .NET officielle). CP-SAT combine propagation de contraintes (filtre le domaine des variables) et recherche arborescente (branch-and-bound). Il est :
Véridique : le plan retourné satisfait toutes les contraintes (pas d’approximation).
Certifiant : le statut de sortie (Optimal) atteste que l’objectif atteint ne peut pas être amélioré.
Efficace : des instances de taille réaliste se traitent en secondes sur CPU.
Branche .NET : package NuGet officiel Google.OrTools, intégration native via #r "nuget: ..." en .NET Interactive.
L’objectif de la Tranche 2 est de passer from-scratch -> CP-SAT sur la même instance, sans reformuler le problème (mêmes tâches, mêmes ressources, mêmes durées). Sur cette instance précise, l’apport du moteur n’est pas un makespan plus court — le glouton était déjà optimal — mais la preuve d’optimalité et l’explication de l’écart à la borne inférieure : la certification, pas l’amélioration.
Tranche 2 : RCPSP via Google.OrTools CP-SAT (moteur .NET natif)
La Partie 5 ci-dessus résout l’ordonnancement de l’atelier (5 tâches A/B/C/D/E, capacité 2 machines) avec un scheduler glouton (heuristique LPT, placement au plus tôt respectant la capacité). Le jumeau Python, lui, délègue ce type de problème à OR-Tools CP-SAT (cellules 26-29). Pour que le C# atteigne lui aussi le moteur de production de son écosystème — sans abandonner le glouton qui enseigne les mécanismes — cette seconde tranche résout la même instance via Google.OrTools (CP-SAT natif .NET), avec la contrainte de ressource cumulative AddCumulative.
Critère owner (#10382) : chaque côté atteint un moteur de production de son écosystème — le from-scratch reste en plus, jamais à la place.
#r "nuget: Google.OrTools"// Tranche 2 : meme instance RCPSP que la Partie 5 (cellule 12), via Google.OrTools CP-SAT .NET.using Google.OrTools.Sat;using System.Collections.Generic;using System.Linq;// --- Instance : atelier 5 taches, capacite 2 (meme que cellule 12) ---var jobs =new(string Name,int Dur,int Demand,string[] Preds)[]{("A",3,1,newstring[]{}),("B",3,1,newstring[]{}),("C",3,1,newstring[]{}),("D",2,1,new[]{"A"}),("E",4,2,new[]{"B","C"}),};int CAPACITY =2;int horizon = jobs.Sum(j => j.Dur);Console.WriteLine("Tranche 2 : RCPSP via Google.OrTools CP-SAT (moteur .NET natif)");Console.WriteLine(newstring('=',60));Console.WriteLine($"Taches: {jobs.Length} | Capacite: {CAPACITY} | Horizon: {horizon}");Console.WriteLine("Makespan glouton LPT (Partie 5, cellule 12) = 10");var model =newCpModel();var starts =new Dictionary<string, IntVar>();var ends =new Dictionary<string, IntVar>();// Contrainte de ressource cumulative (AddCumulative builder fluent).var cum = model.AddCumulative(CAPACITY);foreach(var j in jobs){ starts[j.Name]= model.NewIntVar(0, horizon, $"s_{j.Name}"); ends[j.Name]= model.NewIntVar(0, horizon, $"e_{j.Name}");var iv = model.NewIntervalVar(starts[j.Name], j.Dur, ends[j.Name], $"iv_{j.Name}"); cum.AddDemand(iv, j.Demand);// chaque tache consomme sa demande sur la capacite}// (1) Precedences : une tache demarre apres la fin de ses predecesseurs.foreach(var j in jobs)foreach(var p in j.Preds) model.Add(starts[j.Name]>= ends[p]);// (2) Objectif : minimiser le makespan = max de tous les ends.IntVar makespan = model.NewIntVar(0, horizon,"makespan");model.AddMaxEquality(makespan, jobs.Select(j =>(LinearExpr)ends[j.Name]).ToList());model.Minimize(makespan);var solver =newCpSolver();var status = solver.Solve(model);Console.WriteLine($"Statut CP-SAT : {status}");Console.WriteLine($"Makespan OPTIMAL = {solver.Value(makespan)} (glouton LPT = 10, chemin critique = 7)");Console.WriteLine($"Gain vs glouton : {10 - solver.Value(makespan)} unite(s) -> le glouton LPT etait DEJA optimal");Console.WriteLine($"Wall-clock solve : {solver.WallTime():F3}s | branches: {solver.NumBranches()}");Console.WriteLine("\nSchedule CP-SAT optimal :");foreach(var j in jobs){var predStr = j.Preds.Length==0?"-":string.Join(",", j.Preds); Console.WriteLine($" {j.Name}: [{solver.Value(starts[j.Name])}-{solver.Value(ends[j.Name])}] dur={j.Dur} dem={j.Demand} apres=[{predStr}]");}Console.WriteLine("\nApport du moteur de production :");Console.WriteLine("- Le glouton LPT (Partie 5) trouvait 10 SANS preuve d'optimalite.");Console.WriteLine("- CP-SAT PROUVE que 10 est optimal (statut Optimal, branches explorees).");Console.WriteLine("- Pourquoi le chemin critique (7) est inaccessible : E (demande 2) ne peut");Console.WriteLine(" demarrer a t=3 apres B et C qu'en occupant toutes les ressources, donc A doit");Console.WriteLine(" etre differe au-dela de E -> makespan remonte a 10. La borne 7 ignore la ressource.");Console.WriteLine("Le moteur certifie le resultat du glouton et borne l'ecart a la relaxation.");
Installing Packages
Google.OrTools
Tranche 2 : RCPSP via Google.OrTools CP-SAT (moteur .NET natif)
============================================================
Taches: 5 | Capacite: 2 | Horizon: 15
Makespan glouton LPT (Partie 5, cellule 12) = 10
Statut CP-SAT : Optimal
Makespan OPTIMAL = 10 (glouton LPT = 10, chemin critique = 7)
Gain vs glouton : 0 unite(s) -> le glouton LPT etait DEJA optimal
Wall-clock solve : 0,019s | branches: 12
Schedule CP-SAT optimal :
A: [0-3] dur=3 dem=1 apres=[-]
B: [0-3] dur=3 dem=1 apres=[-]
C: [3-6] dur=3 dem=1 apres=[-]
D: [3-5] dur=2 dem=1 apres=[A]
E: [6-10] dur=4 dem=2 apres=[B,C]
Apport du moteur de production :
- Le glouton LPT (Partie 5) trouvait 10 SANS preuve d'optimalite.
- CP-SAT PROUVE que 10 est optimal (statut Optimal, branches explorees).
- Pourquoi le chemin critique (7) est inaccessible : E (demande 2) ne peut
demarrer a t=3 apres B et C qu'en occupant toutes les ressources, donc A doit
etre differe au-dela de E -> makespan remonte a 10. La borne 7 ignore la ressource.
Le moteur certifie le resultat du glouton et borne l'ecart a la relaxation.
Bilan Tranche 2 — ce que le moteur ajoute (et ce qu’il n’ajoute pas)
La comparaison RCPSP-lite glouton (Partie 5) versus Google.OrTools CP-SAT (Tranche 2) porte sur la même instance (5 tâches A–E, une ressource de capacité 2). Lecture honnête de la sortie mesurée ci-dessus :
Makespan glouton LPT = 10, makespan CP-SAT = 10 : gain 0. Sur cette instance, la mesure ne discrimine pas les deux méthodes sur la qualité du schedule — le glouton trouvait déjà l’optimum, avec le même placement que CP-SAT (A[0-3], B[0-3], C[3-6], D[3-5], E[6-10]). Écrire un gain ici contredirait l’output de la cellule précédente.
Ce que CP-SAT ajoute : la preuve. Le statut Optimal (12 branches, 0,019 s) certifie que 10 est le meilleur makespan réalisable — le glouton produisait 10 sans savoir que c’était le mieux. Le moteur explique aussi pourquoi le chemin critique (7) est inatteignable : E mobilise les 2 machines entières, donc la borne inférieure « sans ressource » n’est pas réalisable (voir la sortie détaillée ci-dessus).
Ce que cet exemple ne démontre pas : un gain de makespan. L’écart glouton ↔︎ optimum n’apparaît que sur des instances plus contraintes — et même là, la garantie pire-cas classique de LPT (\(4/3 - 1/(3m)\), Graham 1969) vaut pour P||Cmax (tâches indépendantes, machines identiques), pas pour le RCPSP avec précédences et demandes variables : dans le cadre de ce notebook, le glouton n’a pas de borne d’approximation.
Règle d’or : la Tranche 1 from-scratch est la fondation pédagogique (comprendre comment marche un solveur avant d’en utiliser un) ; la Tranche 2 est la certification et le passage à l’échelle (prouver l’optimalité, viser les instances réelles).
Où aller ensuite ?Google.OrTools est aussi utilisé en Python (Planners-7-OR-Tools.ipynb) et en C# (Planners-7-OR-Tools-Csharp.ipynb). Pour les très grandes instances ou certains critères continus, les alternatives sont les solveurs MIP (CBC, SCIP) ou les métaheuristiques — voir MyIA.AI.Notebooks/Search/MetaGeneticSharp pour la filière métaheuristique C#.
Conclusion
Ce twin C# a reconstruit from-scratch les 4 piliers de la planification temporelle :
Le notebook Python original atteint ces concepts via unified_planning + ortools CP-SAT ; le twin C# les rend transparents et exécutables sans dépendance externe.
Limitations
L’algorithme de scheduling est glouton (LPT), non optimal (RCPSP est NP-difficile).
La table de composition d’Allen complète (213 entrées) n’est pas implémentée (exercice).
La sémantique PDDL 2.1 est conceptuelle (pas de solveur complet).
Exercices
// Exercice 1 : Table de composition d'Allen// TODO etudiant : implementez la table de composition COMPOSE(R1, R2) retourmant// l'ENSEMBLE des relations possibles entre I1 et I3 sachant R1(I1,I2) et R2(I2,I3).// Indice : Allen 1983, Table III. Pour before o before = {before}, etc.// Etape 1 : representer la table par un dictionnaire (R1,R2) -> HashSet<AllenRel>.// Etape 2 : remplir au moins before/after/meets/equals (les cas les plus frequents).publicstatic HashSet<AllenRel>Compose(AllenRel r1, AllenRel r2){// TODO etudiantvar result =new HashSet<AllenRel>();// Indice : before o before = {before}. Commencez par ce cas.return result;}Show("Compose(before,before)",string.Join(",",Compose(AllenRel.Before, AllenRel.Before).Select(AllenAlgebra.RelStr))+" (a completer)");
Compose(before,before): (a completer)
// Exercice 2 : Deadline dans le STN// TODO etudiant : ajoutez une contrainte de deadline (t_C <= T_max) au STN1// et verifiez la consistan ce. Indice : AddConstraint("X0", "C", 0, T_max).// Etape 1 : choisir un T_max (ex: 12). Etape 2 : re-invoquer FloydWarshall.publicstaticboolDeadlineConsistent(STN stn,string node,double deadline){// TODO etudiantreturntrue;// retournez vrai si le STN + deadline reste consistant}Show("Deadline a 12 (a completer)",DeadlineConsistent(stn1,"C",12.0));
Deadline a 12 (a completer): True
// Exercice 3 : Optimalite du makespan (comparaison brute-force)// TODO etudiant : pour le scenario atelier (5 taches), calculez le makespan OPTIMAL// par enumeration des permutations valides (precedence-respecting topological orders)// et comparez avec le makespan glouton ci-dessus.// Indice : enumerer les ordres topologiques, pour chacun simuler ScheduleAll, garder min.// Etape 1 : enumerer les ordres topologiques des taches. Etape 2 : evaluer chacun.publicstaticdoubleOptimalMakespan(List<Task> tasks,double capacity){// TODO etudiantreturndouble.PositiveInfinity;// retournez le makespan minimal}Show("Makespan optimal (a completer)",OptimalMakespan(tasks,2.0));