À la fin de ce notebook, vous saurez : 1. Comprendre le concept de programmation déclarative vs impérative 2. Utiliser Z3.Linq pour exprimer des contraintes avec la syntaxe LINQ 3. Résoudre des problèmes de satisfaction de contraintes (CSP) 4. Optimiser des solutions avec Solve() et Optimize()
Prérequis
.NET 9.0 Interactive
Connaissance de base de LINQ en C#
Notions de logique des contraintes
Durée estimée : 35-45 minutes
Ce notebook explore Z3.Linq, une bibliothèque qui combine le solveur SMT Z3 avec la syntaxe LINQ de C# pour exprimer des contraintes de manière déclarative.
Contexte : Z3.Linq est issu du travail de Ricardo Niepel (2015), enrichi par les contributions de jsboige (arrays, objets hiérarchiques, listes imbriquées, 2018-2023), puis modernisé par endjin (2022+). Le fork MyIntelligenceAgency/Z3.Linq maintient la version utilisée dans cette série.
Imports OK : Z3.Linq (.deploy fork), Microsoft.Z3, System.Linq
Qu’est-ce que Z3.Linq ?
Z3.Linq est une bibliothèque qui combine deux technologies puissantes:
Z3: Un solveur SMT (Satisfiability Modulo Théories) développé par Microsoft Research
Résout des problèmes de satisfaction de contraintes
Supporte les entiers, réels, tableaux, structures de données, etc.
LINQ (Language Integrated Query): Une syntaxe C# pour exprimer des requêtes de manière déclarative
Permet d’écrire les contraintes Z3 avec la syntaxe familière de LINQ
Les expressions lambda sont automatiquement traduites en formules SMT
Avantage principal: Au lieu d’écrire du code impératif pour résoudre un problème, on déclare les contraintes et Z3 trouve une solution (si elle existe).
Cas d’usage typiques: - Résolution de systèmes d’équations - Problèmes de planification et d’ordonnancement - Vérification formelle - Puzzles logiques (Sudoku, n-Queens, etc.)
Architecture de la pipeline Z3.Linq
Le diagramme ci-dessous illustre le chemin complet d’une contrainte LINQ jusqu’à l’objet C# résultat :
flowchart LR
L["Lambda .Where(x => ...)"] --> V["ExpressionVisitor"]
V --> E["BoolExpr Z3"]
E --> S["Solveur SMT"]
S -- SAT --> M["Modèle Z3"]
S -- UNSAT --> N["null"]
M --> T["Objet .NET T"]
Point clé : l’ExpressionVisitor traduit chaque nœud de l’arbre AST C# (BinaryExpression, MemberAccess, opérateurs arithmétiques) en son équivalent Z3. C’est cette couche qui rend la déclaration de contraintes déclarative plutôt qu’impérative — on exprime quoi, pas comment.
Exemple court
using Z3.Linq;using(var ctx =newZ3Context()){var theorem = from t in ctx.NewTheorem<Symbols<int,int,int,int,int>>() where t.X1- t.X2>=1 where t.X1- t.X2<=3 where t.X1==(2* t.X3)+ t.X5 where t.X3== t.X5 where t.X2==6* t.X4 select t;var solution = theorem.Solve();Console.WriteLine("X1 = {0}, X2 = {1}, X3 = {2}, X4 = {3}, X5 = {4}", solution.X1, solution.X2, solution.X3, solution.X4, solution.X5);}
X1 = 3, X2 = 0, X3 = 1, X4 = 0, X5 = 1
Analyse de l’exemple
Ce que fait le code:
Le code définit un système de 5 contraintes linéaires sur 5 variables entières (X1, X2, X3, X4, X5):
Syntaxe LINQ To Z3: - ctx.NewTheorem<Symbols<int, int, int, int, int>>(): Crée un théorème avec 5 variables entières - where t.X1 - t.X2 >= 1: Ajoute une contrainte (traduite en formule SMT) - theorem.Solve(): Demande à Z3 de trouver une affectation satisfaisant toutes les contraintes
Résultat attendu:
Le solveur Z3 trouve une solution (il peut y en avoir plusieurs). L’exécution ci-dessus donne par exemple : - Une affectation valide : X1 = 3, X2 = 0, X3 = 1, X4 = 0, X5 = 1
Note: Z3 est un solveur SMT (Satisfiability Modulo Théories) capable de résoudre des contraintes sur des entiers, des réels, des tableaux, etc. LINQ To Z3 offre une interface C# naturelle pour exprimer ces contraintes. Pour les fondements théoriques de la résolution SMT (DPLL(T)) et la présentation du solveur Z3 lui-même, voir de Moura & Bjørner, Z3: An Efficient SMT Solver (TACAS 2008) et Nieuwenhuis, Oliveras & Tinelli, Solving SAT and SAT Modulo Théories: From an Abstract DPLL Procedure to DPLL(T) (JACM 2006) ; la théorie des tableaux (notebook 4) et le standard SMT-LIB (notebooks 4, 10) sont ancrés dans la section Références du README.
SAT et UNSAT — le concept fondateur d’un solveur
Jusqu’ici, chaque appel à Solve() a renvoyé une solution. Mais la question fondamentale que tranche un solveur SMT n’est pas « quelle est la solution ? » — c’est « une solution existe-t-elle ? ». C’est ce que désigne le S de SMT : Satisfiability.
SAT (satisfiable) : il existe au moins une affectation des variables qui satisfait toutes les contraintes simultanément. Solve() renvoie alors une telle affectation.
UNSAT (unsatisfiable) : aucune affectation ne satisfait les contraintes — elles sont contradictoires. Solve() renvoie alors null (Z3 a prouvé qu’aucune solution n’existe).
Cette distinction est centrale : l’une des réponses les plus utiles d’un solveur est « c’est impossible », et cette réponse est une preuve (pas une absence de résultat). Par exemple, prouver qu’un planning est irréalisable sous certaines contraintes, ou qu’un invariant de sécurité ne peut jamais être violé. Le (si elle existe) de la section précédente prend ici tout son sens.
L’exemple ci-dessous force un système UNSAT : la même variable ne peut pas être à la fois > 5et< 3.
// Exemple : un système UNSAT (insatisfiable)// La variable X1 doit être à la fois > 5 ET < 3 -- contradiction, aucune solution.using(var ctx =newZ3Context()){var theorem = ctx.NewTheorem<Symbols<int,int>>().Where(t => t.X1>5).Where(t => t.X1<3);var solution = theorem.Solve(); Console.WriteLine($"Solve() a renvoyé : {(solution == null ? "null -> le système est UNSAT(aucune affectation possible)" : solution.ToString())}");}
Solve() a renvoyé : null -> le système est UNSAT (aucune affectation possible)
Le problème des missionnaires et cannibales
Énoncé classique: 3 missionnaires et 3 cannibales doivent traverser une rivière à l’aide d’une barque pouvant transporter au maximum 2 personnes. Si à un moment donné, sur l’une des deux rives, les cannibales sont plus nombreux que les missionnaires, ces derniers se font dévorer.
Objectif: Trouver une séquence de traversées permettant à tous de passer sains et saufs.
Modélisation pour Z3.Linq: - Variables: État de chaque rive à chaque étape (nombre de missionnaires et cannibales) - Contraintes: Capacité de la barque, sécurité des missionnaires, état initial/final - Solution: Séquence d’états satisfaisant toutes les contraintes
La classe suivante encode ce problème de manière déclarative pour être résolu par Z3.
Classe de missionnaires et cannibales
Détails de la modélisation
La classe CanibalsAndMissionaries encode le problème en trois parties:
1. Variables d’état
Variable
Type
Signification
Missionaries[i]
int[]
Nombre de missionnaires sur la rive de départ à l’étape i
Canibals[i]
int[]
Nombre de cannibales sur la rive de départ à l’étape i
Length
int
Nombre total d’étapes dans le chemin
Note: La rive d’arrivée se déduit par soustraction: MissionnairesArrivée = NbMissionaries - Missionaries[i]
2. Contraintes du problème
Contraintes globales: - État initial: Tous les missionnaires et cannibales sont sur la rive de départ - État final: Tous sont sur la rive d’arrivée (0 personnes sur la rive de départ)
Contraintes de transition (modèle d’actions): - Étapes paires (i % 2 == 0): La barque traverse de départ vers arrivée → perte de 1 à SizeBoat personnes - Étapes impaires (i % 2 == 1): La barque revient d’arrivée vers départ → gain de 1 à SizeBoat personnes
Contrainte de sécurité (invariant critique):
// Sur chaque rive, jamais plus de cannibales que de missionnaires// (sauf si aucun missionnaire sur la rive)(Missionaries[i]==0|| Missionaries[i]>= Canibals[i])
3. Construction du théorème avec LINQ
La méthode Create() construit progressivement le théorème en ajoutant des clauses Where(): - Chaque appel à Where() ajoute une contrainte SMT - La boucle for génère des contraintes pour chaque étape du chemin - Z3.Linq traduit automatiquement les expressions lambda en formules logiques
Cette approche déclarative contraste avec la programmation impérative classique: on décrit quoi trouver, pas comment le trouver.
Affirmer les contraintes
L’affirmation des contraintes des arbres d’expression LINQ sur la classe d’état et l’étape suivante. On se base pour ça sur la classe Theorem de LINQ To Z3 :
publicclass CanibalsAndMissionaries{// Le nombre de canibales et missionaires (3 dans le problème original)publicint NbMissionaries {get;set;}=3;// La taille de la barque (2 dans le projet original)publicint SizeBoat {get;set;}=2;// La longueur du chemin calculéprivateint _length;//La propriété qui permet d'accéder à la taille du chemin dans Z3publicint Length{ get => _length; set{ _length = value;// Quand la longueur est déterminée par Z3, on initialise les tableaux pour pouvoir récupérer les valeurs Canibals =newint[value]; Missionaries =newint[value];}}// Un tableau contenant à chaque étape le nombre de canibales sur la berge de départpublicint[] Canibals {get;set;}// Un tableau contenant à chaque étape le nombre de missionaires sur la berge de départpublicint[] Missionaries {get;set;}/// <summary>/// Une représentation lisible de la solution proposée/// </summary>/// <returns>une chaine de caractère ou chaque ligne est une étape du chemin</returns>publicoverridestringToString(){var sb =newStringBuilder();for(int i =0; i < Canibals.Length; i++){ sb.AppendLine($"{i + 1} - (({Missionaries[i]}M, {Canibals[i]}C, {1 - i % 2}), ({(i % 2)}, {NbMissionaries - Missionaries[i]}M, {NbMissionaries - Canibals[i]}C))");}return sb.ToString();}/// <summary>/// La méthode qui permet la création du théorème associé au problème/// </summary>/// <param name="context">Le contexte Z3 qui devra interpréter les contraintes</param>/// <param name="entity">Une valeur du problème servant de modèle pour définir les paramètres principaux</param>/// <returns>Un théorème de notre environnement qui peut être filtré et résolu</returns>publicstatic Theorem<CanibalsAndMissionaries>Create(Z3Context context, CanibalsAndMissionaries entity){// On créée une instance du théorème, sans contraintes, puis on va rajouter les contraintes une à unevar theorem = context.NewTheorem<CanibalsAndMissionaries>();// Contraintes globales// On récupère les contraintes globales qui seront injectées sous forme de constante dans la lambda expressionvar sizeBoat = entity.SizeBoat;int nbMissionaries = entity.NbMissionaries;int maxlength = entity.Length; theorem = theorem.Where(caM => caM.NbMissionaries== nbMissionaries); theorem = theorem.Where(caM => caM.SizeBoat== sizeBoat);// Etat initial theorem = theorem.Where(caM => caM.Missionaries[0]== caM.NbMissionaries&& caM.Canibals[0]== caM.NbMissionaries);//Modèle de transition// On filtre à chaque étape selon les actions possiblefor(int iclosure =0; iclosure < maxlength; iclosure++){var i = iclosure;//Les deux rives contiennent entre 0 et 3 personnes theorem = theorem.Where(caM => caM.Canibals[i]>=0&& caM.Canibals[i]<= caM.NbMissionaries&& caM.Missionaries[i]>=0&& caM.Missionaries[i]<= caM.NbMissionaries);if(i %2==0){// Aux itérations paires, la rive de départ perd entre 1 et SizeBoat personnes theorem = theorem.Where(caM => caM.Canibals[i +1]<= caM.Canibals[i]&& caM.Missionaries[i +1]<= caM.Missionaries[i]&& caM.Canibals[i +1]+ caM.Missionaries[i +1]- caM.Canibals[i]- caM.Missionaries[i]<0&& caM.Canibals[i +1]+ caM.Missionaries[i +1]- caM.Canibals[i]- caM.Missionaries[i]>=-caM.SizeBoat);}else{// Aux itérations impaires, la rive de départ gagne entre 1 et SizeBoat personnes theorem = theorem.Where(caM => caM.Canibals[i +1]>= caM.Canibals[i]&& caM.Missionaries[i +1]>= caM.Missionaries[i]&& caM.Canibals[i +1]+ caM.Missionaries[i +1]- caM.Canibals[i]- caM.Missionaries[i]>0&& caM.Canibals[i +1]+ caM.Missionaries[i +1]- caM.Canibals[i]- caM.Missionaries[i]<= caM.SizeBoat);}//Jamais moins de missionnaire que de cannibal sur chacune des rives theorem = theorem.Where(caM =>(caM.Missionaries[i]==0||(caM.Missionaries[i]>= caM.Canibals[i]))&&(caM.Missionaries[i]== caM.NbMissionaries||((caM.NbMissionaries- caM.Missionaries[i])>=(caM.NbMissionaries- caM.Canibals[i]))));}// Test de but// A l'arrivée, plus personne sur la rive de départ theorem = theorem.Where( caM => caM.Length>0&& caM.Length< maxlength&& caM.Missionaries[caM.Length-1]==0&& caM.Canibals[caM.Length-1]==0);return theorem;}}
Obtenir la solution
LINQ To Z3 nous donne la solution sous forme d’ un objet POCO (Plain Old CLR Object) du type de paramètre générique T du théorème.
using System.Diagnostics;var stopWatch =newStopwatch(); stopWatch.Start(); TimeSpan debutChrono;// Solving Canibals & Missionairesvar can =newCanibalsAndMissionaries(){NbMissionaries =3, SizeBoat =2, Length =30};using(var ctx =newZ3Context()){var theorem = CanibalsAndMissionaries.Create(ctx, can); debutChrono = stopWatch.Elapsed;//Print(theorem);var result = theorem.Solve();// affichage du résultatdisplay($"Durée Cannibales et Missionaires {stopWatch.Elapsed - debutChrono}");display(result);}
Le code ci-dessus résout le problème classique des 3 missionnaires et 3 cannibales avec une barque de capacité 2.
Paramètres du problème: - NbMissionaries = 3: 3 missionnaires et 3 cannibales - SizeBoat = 2: La barque peut transporter au maximum 2 personnes - Length = 30: Limite supérieure du nombre d’étapes à explorer
Sortie attendue: - Un chemin solution avec le nombre d’étapes nécessaires - Chaque ligne au format: (MissionnairesDépart, CannibalsDépart, Barque), (Barque, MissionnairesArrivée, CannibalesArrivée) - La durée d’exécution du solveur Z3
Note: La méthode Solve() trouve une solution satisfaisant les contraintes, mais pas nécessairement la plus courte. Pour obtenir la solution optimale, il faut utiliser Optimize() (voir section suivante).
Exercice 2 : Problème de dimensionnement (n-Queens lite)
Il s’agit de placer N reines sur un echiquier NxN de sorte qu’aucune ne soit sur la même ligne, colonne ou diagonale qu’une autre.
Objectif : Modelisez un echiquier 4x4 avec Z3.Linq. Chaque variable represente la colonne de la reine sur la ligne correspondante.
Indices : - Utilisez Symbols<int, int, int, int> pour un echiquier 4x4 - Contrainte 1 : toutes les colonnes différentes (Q1 != Q2 != Q3 != Q4) - Contrainte 2 : pas de diagonale => |Qi - Qj| != |i - j| - Utilisez Math.Abs pour la valeur absolue des différences
// Exercice 2 : n-Queens 4x4 avec Z3.Linq// TODO etudiant : placez 4 reines sur un echiquier 4x4// Etape 1 : definissez un theoreme avec 4 variables (Q1, Q2, Q3, Q4)// Etape 2 : ajoutez les contraintes de colonnes distinctes// Etape 3 : ajoutez les contraintes anti-diagonales// Indice : Math.Abs(Qi - Qj) != Math.Abs(i - j) pour i != j// using (var ctx = new Z3Context())// {// var theorem = from q in ctx.NewTheorem<Symbols<int, int, int, int>>()// where ... // Vos contraintes de colonnes distinctes// where ... // Vos contraintes anti-diagonales// select q;// var solution = theorem.Solve();// Console.WriteLine($"Q1={solution.X1}, Q2={solution.X2}, Q3={solution.X3}, Q4={solution.X4}");// }Console.WriteLine("Exercice a completer : n-Queens 4x4 avec Z3.Linq");
Exercice a completer : n-Queens 4x4 avec Z3.Linq
Recherche de la solution la plus courte
using System.Diagnostics;var stopWatch =newStopwatch(); stopWatch.Start(); TimeSpan debutChrono;// Solving Canibals & Missionairesvar can =newCanibalsAndMissionaries(){NbMissionaries =3, SizeBoat =2, Length =30};using(var ctx =newZ3Context()){var theorem = CanibalsAndMissionaries.Create(ctx, can); debutChrono = stopWatch.Elapsed;//Print(theorem);var result = theorem.Optimize(Optimization.Minimize, objMnC => objMnC.Length);// affichage du résultatdisplay($"Durée Cannibales et Missionaires {stopWatch.Elapsed - debutChrono}");display(result);}
Solve(): Trouve une solution satisfaisant toutes les contraintes, sans garantie d’optimalité
Optimize(Optimization.Minimize, objMnC => objMnC.Length): Trouve la solution avec la plus petite valeur de Length
Dans le cas des missionnaires et cannibales, cela signifie: - Solve() peut retourner un chemin de 15 étapes alors qu’il existe un chemin de 11 étapes - Optimize() garantit de trouver le chemin le plus court possible
Performance: L’optimisation est plus coûteuse en temps de calcul, car le solveur doit explorer l’espace de recherche pour prouver qu’aucune solution plus courte n’existe.
Résultat attendu: Le chemin optimal pour 3 missionnaires et 3 cannibales avec une barque de capacité 2 est de 11 étapes.
using System.Diagnostics;var stopWatch =newStopwatch(); stopWatch.Start(); TimeSpan debutChrono;// Solving Canibals & Missionairesvar can =newCanibalsAndMissionaries(){NbMissionaries =30, SizeBoat =7, Length =100};using(var ctx =newZ3Context()){var theorem = CanibalsAndMissionaries.Create(ctx, can); debutChrono = stopWatch.Elapsed;//Print(theorem);var result = theorem.Optimize(Optimization.Minimize, objMnC => objMnC.Length);// affichage du résultatdisplay($"Durée Cannibales et Missionaires {stopWatch.Elapsed - debutChrono}");display(result);}
Exercice 3 : Variante du problème - Loups, Chevres et Choux
Generalisez le modèle des missionnaires-cannibales pour resoudre un problème voisin : un fermier doit traverser une riviere avec un loup, une chevre et un chou. La barque ne peut transporter que le fermier plus un seul item. Le loup mange la chevre et la chevre mange le chou s’ils restent seuls ensemble.
Objectif : Adaptez la classe CanibalsAndMissionaries ou créez un nouveau modèle Z3.Linq pour ce problème.
Indices : - Le fermier est toujours dans la barque (pas besoin de variable separree, il est implicite) - 3 items a transporter : L (loup), C (chevre), X (chou) - Invariant : le fermier ne peut pas laisser (L+C) ou (C+X) seuls sur une rive - Utilisez un codage similaire avec des tableaux pour les positions a chaque étape
// Exercice 3 : Loups, Chevres et Choux avec Z3.Linq// TODO etudiant : modelisez le probleme du fermier, loup, chevre et chou// Etape 1 : definissez les variables d'etat (positions de chaque item par etape)// Etape 2 : ajoutez les contraintes de transition (1 seul item par traverse)// Etape 3 : ajoutez les invariants de securite (pas L+C ni C+X seuls)// Etape 4 : resolvez avec Optimize pour trouver le chemin le plus court// Indice : vous pouvez utiliser Symbols<int, int, int> pour 3 positions// ou creer une classe adaptee avec un tableau de positions// public class FarmerProblem { ... }// using (var ctx = new Z3Context()) { ... }Console.WriteLine("Exercice a completer : loups, chevres et choux avec Z3.Linq");
Exercice a completer : loups, chevres et choux avec Z3.Linq
B9 - Environnements record positionnels (DSL NewTheorem<PositionalRecord>)
L’environnement d’un théorème (le type T passe a NewTheorem<T>) peut etre un record positionnel C# – par exemple record Point(int X, int Y) – et non une class mutable. La reconstruction de la solution passe alors par une branche dediee du solveur (Theorem.cs:993-1099) qui selectionne le primary constructor du record, evalue chaque paramètre depuis le modèle Z3, puis invoque Activator.CreateInstance(t, args) avec les valeurs positionnelles – l’ancienne branche Activator.CreateInstance(t) throw sur un record sans constructeur sans paramètre (un positional record n’expose que des init-only setters).
Le mini-problème : sur un point (X, Y) dans le plan, trouver le point satisfaisant X + Y = 12etX = 2·Y (solution unique attendue : X = 8, Y = 4).
Stack : A+B – le raw doit gerer manuellement la reconstruction via Model.Eval sur chaque IntExpr, tandis que le DSL NewTheorem<Point>().Where(...) invoque la branche ConstructFromModel automatiquement ; l’objet rendu (Point { X = 8, Y = 4 }) est type C#, pas une paire de IntNum.
Ancre fork : Theorem.cs:993-1099 (branche ConstructFromModel), tests Z3.Linq.Tests/RecordEnvTheoryTests.cs:18-91 (4 cas : positional standard, scalaires mixtes int+bool, trois paramètres, UNSAT). La feature etait listee dans le backlog B9 d’#4688 avec une ancre « a confirmer » – le grep la trouve dans la branche reconstruction, pas dans la declaration de type (qui est du C# standard).
// B9 - Record positionnel "deux stacks" sur le meme probleme (X+Y=12, X=2Y).// Un point (X, Y) dans le plan, soumis a 2 contraintes lineaires.// Solution unique attendue : X=8, Y=4.// Un record positionnel declare au niveau de la cellule (requis par NewTheorem<T>).public record PointRecord(int X,int Y){}// =====================================================================// BLOC DSL (Z3.Linq) : la reconstruction d'un record via primary ctor// ====================================================================={usingvar thCtx =newZ3Context();var pt = thCtx.NewTheorem<PointRecord>().Where(p => p.X+ p.Y==12).Where(p => p.X==2* p.Y).Solve(); Console.WriteLine($"[DSL] NewTheorem<PointRecord>(X+Y=12, X=2Y) : {(pt == null ? "UNSAT" : $"SAT ->{pt}(X={pt.X}, Y={pt.Y})")}");// Variante discriminante : reorder les Where -> la solution NE change PAS// (le matching des params se fait par nom via l'environnement, pas par l'ordre des Where)var pt2 = thCtx.NewTheorem<PointRecord>().Where(p => p.X==2* p.Y).Where(p => p.X+ p.Y==12).Solve(); Console.WriteLine($"[DSL] (memo) Where inverse : {(pt2 == null ? "UNSAT" : $"SAT ->{pt2}(meme X, Y)")}");// Variante UNSAT : X >= 0 et X < 0 -> le binding doit rendre null, pas throwvar unsat = thCtx.NewTheorem<PointRecord>().Where(p => p.X>=0).Where(p => p.X<0).Solve(); Console.WriteLine($"[DSL] cas UNSAT (X>=0 et X<0) : {(unsat == null ? "null(UNSAT, pas d'exception)" : "??")}");}// =====================================================================// BLOC RAW (API brute Microsoft.Z3) : meme probleme, reconstruction manuelle// ====================================================================={usingvar z3 =new Microsoft.Z3.Context();var x = z3.MkIntConst("X");var y = z3.MkIntConst("Y");var solver = z3.MkSolver(); solver.Assert(z3.MkEq(z3.MkAdd(x, y), z3.MkInt(12))); solver.Assert(z3.MkEq(x, z3.MkMul(z3.MkInt(2), y))); Status st = solver.Check(); Console.WriteLine($"[RAW] X+Y=12, X=2Y : {st}");if(st == Status.SATISFIABLE){var m = solver.Model;// Reconstruction manuelle : 2 Eval + parse IntNum + construction (x, y)var xv =((IntNum)m.Eval(x,true)).Int;var yv =((IntNum)m.Eval(y,true)).Int; Console.WriteLine($"[RAW] temoin : (X={xv}, Y={yv}) (construction manuelle a partir du Model)");}// Cas UNSAT symetriquevar sUn = z3.MkSolver(); sUn.Assert(z3.MkGe(x, z3.MkInt(0))); sUn.Assert(z3.MkLt(x, z3.MkInt(0))); Console.WriteLine($"[RAW] X>=0 et X<0 : {sUn.Check()} (aucune affectation)");}
[DSL] NewTheorem<PointRecord>(X+Y=12, X=2Y) : SAT -> PointRecord { X = 8, Y = 4 } (X=8, Y=4)
[DSL] (memo) Where inverse : SAT -> PointRecord { X = 8, Y = 4 } (meme X, Y)
[DSL] cas UNSAT (X>=0 et X<0) : null (UNSAT, pas d'exception)
[RAW] X+Y=12, X=2Y : SATISFIABLE
[RAW] temoin : (X=8, Y=4) (construction manuelle a partir du Model)
[RAW] X>=0 et X<0 : UNSATISFIABLE (aucune affectation)
Lecture B9 - deux ecritures d’un même record, un même résultat
Les deux ecritures resolvent le même système (X + Y = 12, X = 2·Y) et produisent le même temoin (X = 8, Y = 4), mais leur cout d’ecriture est asymetrique :
Aspect
Stack B (raw Microsoft.Z3)
Stack A (DSL Z3.Linq)
Variables
MkIntConst("X"), MkIntConst("Y") declaratives une a une
p => p.X + p.Y == 12, p => p.X == 2*p.Y en LINQ naturel
Verification
MkSolver().Assert(...).Check() + cast IntNum/parse dans le Model
.Solve() typé C#
Reconstruction
model.Eval(x, true).ToString() + cast manuel + reconstruction (x, y) a la main
un simple Point { X = 8, Y = 4 } – le binding a fait le reste
Surface code
plusieurs lignes significatives + risque d’erreur de typage des IntNum
quelques lignes de contraintes
Constructeur sans paramètre
non requis (le raw n’instancie jamais d’objet C#)
necessaire dans le cas d’une class mutable, interdit dans le cas d’un record (l’ancienne branche Activator.CreateInstance(t) throw)
Ligne de contraste : la valeur ajoutee du DSL ici n’est pas une fecondation d’expression (comme Sum qui replie N termes en un seul MkAdd), mais le routage de reconstruction – la detection du type d’environnement (class mutable avec setter vs record positionnel vs type anonyme) et l’invocation de la bonne branche de Theorem.cs:993-1099. Cote raw, c’est au chercheur de coder la reconstruction – cote DSL, elle est incluse par defaut dans NewTheorem<T>(). Les tests unitaires RecordEnvTheoryTests (4 cas) verifient que la branche positionnelle couvre records a 2-3 params, scalaires mixtes (int + bool), et le retour null sur UNSAT.
Exercice 4 : record positionnel a 3 paramètres (stability of constructor order)
Reproduisez la demarche ci-dessus avec un record a trois paramètres – par exemple record Triple(int A, int B, int C) – et trois contraintes scalaires (par exemple A == 1, B == 2, C == 3). Le solveur doit rendre un Triple { A = 1, B = 2, C = 3 }.
Variante discriminante : declarez les contraintes dans un ordre différent (par exemple B == 2, C == 3, A == 1). La solution reste-t-elle la même ? Le matching des arguments du constructeur se fait-il par nom ou par position dans le .Where ?
Indice : voir Theorem.cs:1076 – la reconstruction selectionne le constructeur dont tous les paramètres sont dans l’environnement, puis passe les args positionnellement.
// Exercice 4 : record positionnel a 3 parametres avec Z3.Linq// TODO etudiant : modelisez un record Triple(int A, int B, int C) puis :// Etape 1 : declarez record Triple(int A, int B, int C) au niveau de la cellule// Etape 2 : 3 contraintes scalaires : A == 1, B == 2, C == 3// Etape 3 : lancez .Solve() et verifiez que vous obtenez Triple { A=1, B=2, C=3 }// Etape 4 (variante discriminante) : changez l'ordre des contraintes en B==2, C==3, A==1// et verifiez que la solution reste la meme (le matching des params se fait par NOM, pas par ordre du Where)// Indice : la reconstruction du record (Theorem.cs:1076) selectionne le constructeur// dont TOUS les parametres sont satisfiables par l'environnement ; le passage des args// est positionnel. L'ordre de declaration des Where ne change donc pas la solution,// seul l'ordre des parametres du constructeur compte.Console.WriteLine("Exercice a completer : record Triple positionnel a 3 parametres avec Z3.Linq");
Exercice a completer : record Triple positionnel a 3 parametres avec Z3.Linq
Conclusion et analyse comparative
Ce notebook a illustré l’utilisation de Z3.Linq pour résoudre des problèmes de satisfaction de contraintes en combinant la puissance du solveur SMT Z3 avec la syntaxe LINQ de C#.
Comparaison des approches
Approche
Cas simple (3M, 3C)
Cas complexe (30M, 30C)
Avantages
Limitations
Solve()
Rapide
Rapide
Trouve une solution
Pas forcément optimale
Optimize()
Très rapide
Plus lent
Solution optimale
Coût de calcul plus élevé
Points clés à retenir
Modélisation déclarative: Les contraintes sont exprimées naturellement avec LINQ
Abstraction puissante: Z3.Linq traduit automatiquement les expressions LINQ en formules SMT
Optimisation: La méthode Optimize() permet de minimiser/maximiser des objectifs
Scalabilité: Le solveur gère des instances de taille significative (30 personnes)
Note pratique: Pour des problèmes de grande taille, il est recommandé de fournir une borne supérieure réaliste pour Length afin d’éviter l’explosion combinatoire.
Applications possibles
Planification de tâches
Problèmes de routage et d’allocation de ressources
Vérification formelle de propriétés
Résolution de puzzles logiques (Sudoku, n-Queens, etc.)
Exercice : Modélisez un Problème de Planification
Modélisez et résolvez un problème de planification avec Z3.Linq.
Objectifs
Créez un solveur pour planifier des tâches sur plusieurs machines.
Instructions
Questions d’analyse
Combien de machines sont utilisées dans la solution optimale ?
Quelle est la différence entre Solve() et Optimize() pour ce problème ?
Comment ajouter une contrainte “tâches A et b ne peuvent pas être sur la même machine” ?
// TODO: Définissez le problème de planification// - 5 tâches avec des durées différentes// - 3 machines avec des capacités différentes// - Contrainte : chaque tâche sur une seule machine// - Contrainte : capacité machine non dépassée// - Objectif : minimiser le temps total de complétionpublicclass PlanningProblem{// TODO: Propriétés du problème// public int[] TaskDurations { get; set; }// public int[] MachineCapacities { get; set; }// public int[,] Assignment { get; set; } // tâche -> machine}// TODO: Créez le théorème LINQ pour le problème// var theorem = from t in ctx.NewTheorem<PlanningProblem>()// where ... // Vos contraintes// select t;// TODO: Résolvez et affichez la solution// var solution = theorem.Optimize(...);Console.WriteLine("Exercice a completer : modeliser un probleme de planification avec Z3.Linq");
Exercice a completer : modeliser un probleme de planification avec Z3.Linq