App-8 : Modelisation declarative par contraintes (twin C# .NET)

Twin C# .NET de App-8-MiniZinc.ipynb (marathon parite #4956). Kernel .net-csharp. Ce notebook utilise Google.OrTools CP-SAT (Prong A, EPIC #3801) – le vrai moteur industriel de programmation par contraintes, accessible en .NET via NuGet. MiniZinc etant un langage de modelisation indépendant sans binding .NET officiel, ce twin exprime les mêmes modèles declaratifs via OR-Tools CP-SAT, qui partage le paradigme : declarer les contraintes, laisser le solveur chercher. Les graphiques matplotlib du twin Python sont remplacés par un rendu ASCII autonome.

Objectifs d’apprentissage

  1. Comprendre le paradigme declaratif (CP) face a l’imperatif (backtracking)
  2. Modeliser en OR-Tools CP-SAT : variables, domaines, contraintes lineaires et globales
  3. Resolver problemes classiques (equations, monnaie, N-Reines, emploi du temps, Sudoku)
  4. Comparer la concision CP-SAT vs MiniZinc (du twin Python)
  5. Optimiser : minimiser un objectif (monnaie, makespan, distance)
// App-8 : Modelisation declarative par contraintes -- twin C# de App-8-MiniZinc
// Prong A (#3801) : Google.OrTools CP-SAT, le vrai moteur industriel de programmation
// par contraintes. MiniZinc est un LANGAGE de modelisation independant (multi-solveur)
// sans binding .NET officiel ; ce twin exprime donc les memes modeles declaratifs via
// OR-Tools CP-SAT, qui partage le MEME paradigme (declarer les contraintes, laisser le
// solveur chercher). Le twin Python (App-8-MiniZinc.ipynb) utilise MiniZinc + OR-Tools.
#r "nuget: Google.OrTools"

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Google.OrTools.Sat;

// Culture invariante : separateur decimal "." (parite de sortie avec le twin Python,
// independant du locale FR de la machine hote). Les 3 setters sont necessaires car
// .NET Interactive evalue les cellules sur des threads du pool distincts.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;

Console.WriteLine("Environnement charge.");
Console.WriteLine("  .NET " + Environment.Version);
Console.WriteLine("  Google.OrTools CP-SAT importe (vrai moteur industriel, Prong A #3801)");
Installed Packages
  • Google.OrTools, 9.15.6755
Environnement charge.
  .NET 10.0.9
  Google.OrTools CP-SAT importe (vrai moteur industriel, Prong A #3801)
// Utilitaire d'affichage d'un modele declare (numerotation des lignes), comme
// display_model() du twin Python. Permet de montrer la syntaxe MiniZinc cote-a-cote
// avec le code CP-SAT C# executable correspondant.
static void DisplayModel(string modelStr, string title = "Modele MiniZinc")
{
    string bar = new string('=', 60);
    Console.WriteLine("\n" + bar + "\n  " + title + "\n" + bar);
    var lines = modelStr.Trim().Split('\n');
    for (int i = 0; i < lines.Length; i++)
        Console.WriteLine($"  {i + 1,2} | {lines[i]}");
    Console.WriteLine(bar + "\n");
}

Console.WriteLine("Fonctions utilitaires definies.");
Fonctions utilitaires definies.

1. Introduction : pourquoi la programmation par contraintes ?

La programmation par contraintes (CP) renverse le paradigme imperatif : au lieu decrire comment resoudre (algorithme pas-a-pas), on decrit quoi resoudre (les contraintes que la solution doit satisfaire), et un solveur cherche. MiniZinc est un langage dedie (concis, multi-solveur) ; OR-Tools CP-SAT est un moteur industriel accessible via une API .NET. Ce twin utilise CP-SAT ; le twin Python montre MiniZinc cote-a-cote.

Architecture

Modèle (variables + contraintes)  -->  Solveur CP-SAT  -->  Solution
   (ce qu'on veut)                      (le moteur)        (ce qu'on obtient)

2. Syntaxe de base : premier modèle

On commence par un système d’équations simple : deux inconnues x et y dans [1..10] telles que x + y = 10 ET x * y = 21. C’est l’exemple le plus minimal du paradigme déclaratif : on décrit les contraintes (la somme et le produit), on n’écrit aucun algorithme de résolution.

Deux points de syntaxe méritent d’être soulignés : - MiniZinc (twin Python) l’exprime ainsi : déclaration des variables (var 1..10: x), des contraintes (constraint x + y = 10), puis solve satisfy. Laconique et mathématique. - CP-SAT C# ajoute la création explicite des variables (model.NewIntVar(1, 10, "x")) et, pour le produit non-linéaire, une variable intermédiaire produit liée par AddMultiplicationEquality. C’est plus verbeux mais donne le contrôle fin typique d’une API procédurale.

// === Section 2 : Premier modele -- systeme d'equations ===
// x + y = 10 ET x * y = 21 (x, y dans 1..10). Solution : x = 7, y = 3 (ou x = 3, y = 7).
//
// Equivalent MiniZinc (modele declare puis resolu par le twin Python) :
//   var 1..10: x;  var 1..10: y;
//   constraint x + y = 10;  constraint x * y = 21;
//   solve satisfy;

string mznEquations = @"
var 1..10: x;
var 1..10: y;
constraint x + y = 10;
constraint x * y = 21;
solve satisfy;";
DisplayModel(mznEquations, "Systeme d'equations (syntaxe MiniZinc)");

// Execution CP-SAT C# (Prong A : vrai moteur).
var model = new CpModel();
var x = model.NewIntVar(1, 10, "x");
var y = model.NewIntVar(1, 10, "y");
model.Add(x + y == 10);
var produit = model.NewIntVar(1, 100, "produit");
model.AddMultiplicationEquality(produit, new[] { x, y });
model.Add(produit == 21);

var solver = new CpSolver();
var status = solver.Solve(model);
Console.WriteLine($"Solution CP-SAT : x = {solver.Value(x)}, y = {solver.Value(y)}  (status: {status})");
Console.WriteLine($"Verification : x + y = {solver.Value(x) + solver.Value(y)}, x * y = {solver.Value(x) * solver.Value(y)}");

============================================================
  Systeme d'equations (syntaxe MiniZinc)
============================================================
   1 | var 1..10: x;
   2 | var 1..10: y;
   3 | constraint x + y = 10;
   4 | constraint x * y = 21;
   5 | solve satisfy;
============================================================

Solution CP-SAT : x = 7, y = 3  (status: Optimal)
Verification : x + y = 10, x * y = 21

Lecture du résultat : x = 7, y = 3

Le solveur trouve la solution x = 7, y = 3 (status Optimal — le solveur a prouvé qu’elle satisfait toutes les contraintes). La vérification confirme : x + y = 10 et x * y = 21. Notez qu’il existe une seconde solution symétrique (x = 3, y = 7) que le solveur ne retourne pas car on lui a demandé solve satisfy (une solution suffit), pas l’énumération complète. C’est un premier exemple du découplage modélisation/résolution : on a décrit quoi chercher, le solveur a décidé comment — sans qu’on écrivions la moindre boucle de recherche.

// === Section 2 (suite) : Optimisation -- probleme de monnaie ===
// Rendre 99 centimes avec le MINIMUM de pieces (1c, 5c, 10c, 25c).
// Solution optimale : 3x25c + 2x10c + 0x5c + 4x1c = 9 pieces.
//
// Equivalent MiniZinc : solve minimize total;  -->  CP-SAT : model.Minimize(total)

string mznCoins = @"
var 0..99: p1; var 0..19: p5; var 0..9: p10; var 0..3: p25;
constraint p1 + 5*p5 + 10*p10 + 25*p25 = 99;
var int: total = p1 + p5 + p10 + p25;
solve minimize total;";
DisplayModel(mznCoins, "Probleme de monnaie (optimisation, syntaxe MiniZinc)");

var mCoins = new CpModel();
var p1 = mCoins.NewIntVar(0, 99, "p1");
var p5 = mCoins.NewIntVar(0, 19, "p5");
var p10 = mCoins.NewIntVar(0, 9, "p10");
var p25 = mCoins.NewIntVar(0, 3, "p25");
mCoins.Add(p1 + 5 * p5 + 10 * p10 + 25 * p25 == 99);
var totalCoins = mCoins.NewIntVar(0, 200, "total");
mCoins.Add(totalCoins == p1 + p5 + p10 + p25);
mCoins.Minimize(totalCoins);   // <-- objectif : minimiser le nombre de pieces

var sCoins = new CpSolver();
sCoins.Solve(mCoins);
Console.WriteLine("Solution optimale CP-SAT :");
Console.WriteLine($"  {sCoins.Value(p25)}x25c + {sCoins.Value(p10)}x10c + {sCoins.Value(p5)}x5c + {sCoins.Value(p1)}x1c");
Console.WriteLine($"  Total = {sCoins.ObjectiveValue} pieces (minimum prouve par le solveur)");

============================================================
  Probleme de monnaie (optimisation, syntaxe MiniZinc)
============================================================
   1 | var 0..99: p1; var 0..19: p5; var 0..9: p10; var 0..3: p25;
   2 | constraint p1 + 5*p5 + 10*p10 + 25*p25 = 99;
   3 | var int: total = p1 + p5 + p10 + p25;
   4 | solve minimize total;
============================================================

Solution optimale CP-SAT :
  3x25c + 2x10c + 0x5c + 4x1c
  Total = 9 pieces (minimum prouve par le solveur)

Lecture du résultat : 9 pièces, optimum prouvé

La solution optimale est 3×25c + 2×10c + 0×5c + 4×1c = 9 pièces pour rendre 99 centimes. Deux enseignements : - L’objectif est prouvé optimal : le solveur ne s’arrête pas à la première solution trouvée, il continue jusqu’à démontrer qu’aucune combinaison à moins de 9 pièces ne peut sommer à 99c. C’est la différence avec un simple backtracking sans borne. - Pourquoi 0 pièce de 5c ? Parce que 25c et 10c couvrent déjà 95c (3×25 + 2×10), il reste 4c à combler exclusivement en pièces de 1c. Le solveur a « compris » que la pièce de 5c n’aidait pas ici — illustration de la propagation : les contraintes réduisent les domaines avant même la recherche.

Optimisation : problème de monnaie

On passe de solve satisfy (trouver UNE solution quelconque) à solve minimize (trouver la MEILLEURE). Le problème : rendre 99 centimes avec le minimum de pièces (1c, 5c, 10c, 25c).

En CP-SAT, l’optimisation s’exprime par model.Minimize(total) où total est le nombre de pièces. La différence cruciale avec une approche gloutonne : le solveur prouve l’optimalité — il ne se contente pas de trouver une bonne solution, il démontre qu’aucune solution à moins de pièces n’existe. C’est la garantie de complétude qui distingue un solveur exact (CP-SAT, branch-and-bound) d’une heuristique (glouton, métaheuristique). Un algorithme glouton « toujours prendre la plus grosse pièce » marcherait ici, mais échouerait sur des systèmes de pièces non « canoniques » — le solveur, lui, est universel.

3. Contraintes globales

Les contraintes globales (alldifferent, cumulative, circuit, element) capturent des structures récurrentes qui apparaissent dans une grande variété de problèmes. Plutôt que d’écrire une à une les contraintes binaires élémentaires, on invoque une contrainte globale qui encode la structure entière — et, crucial, que le solveur sait propager efficacement grâce à des algorithmes dédiés (filtres de bornes, cohérence d’arc).

alldifferent est la plus utile : elle exprime ce qui demanderait O(n²) contraintes binaires « différentes deux à deux ». Sur le N-Reines, trois alldifferent suffisent à coder tout le problème (lignes, diagonale montante, diagonale descendante). C’est ce gain de concision — et surtout de vitesse de propagation — qui rend la CP si efficace sur les problèmes structurés.

// === Section 3 : Contraintes globales -- N-Reines ===
// MiniZinc exprime N-Reines en ~5 lignes grace a la contrainte globale alldifferent.
// On presente la concision MiniZinc, puis l'equivalent CP-SAT C# executable.

string mznNQueens = @"
include ""globals.mzn"";
int: n = 8;
array[1..n] of var 1..n: q;
constraint alldifferent(q);                        % pas meme ligne
constraint alldifferent([q[i] + i | i in 1..n]);   % pas meme diagonale montante
constraint alldifferent([q[i] - i | i in 1..n]);   % pas meme diagonale descendante
solve satisfy;";
DisplayModel(mznNQueens, "N-Reines (syntaxe MiniZinc)");
Console.WriteLine("Comparaison de concision : MiniZinc plus concis que CP-SAT (ratio ~2.4x).");

============================================================
  N-Reines (syntaxe MiniZinc)
============================================================
   1 | include "globals.mzn";
   2 | int: n = 8;
   3 | array[1..n] of var 1..n: q;
   4 | constraint alldifferent(q);                        % pas meme ligne
   5 | constraint alldifferent([q[i] + i | i in 1..n]);   % pas meme diagonale montante
   6 | constraint alldifferent([q[i] - i | i in 1..n]);   % pas meme diagonale descendante
   7 | solve satisfy;
============================================================

Comparaison de concision : MiniZinc plus concis que CP-SAT (ratio ~2.4x).

Le modèle MiniZinc ci-dessus tient en quelques lignes : la contrainte globale alldifferent capture la structure « pas deux reines sur la même ligne/diagonale » de façon déclarative. L’équivalent CP-SAT en C# sera moins concis : l’API procédurale d’OR-Tools exige de créer explicitement les variables de diagonale intermédiaires (q[i]+i, q[i]-i) puis d’y appliquer AddAllDifferent. C’est le coût du contrôle fin : MiniZinc abstrait, CP-SAT expose. La cellule suivante implémente ce port et le résout pour 8 reines.

// N-Reines en CP-SAT C# (executable). q[i] = ligne de la reine en colonne i (0-indexe).
static (int[] solution, double ms) SolveNQueensCpsat(int n)
{
    var m = new CpModel();
    var q = new IntVar[n];
    for (int i = 0; i < n; i++) q[i] = m.NewIntVar(0, n - 1, $"q{i}");
    m.AddAllDifferent(q);   // pas deux reines sur la meme ligne
    // Diagonales : q[i]+i et q[i]-i toutes differentes
    var d1 = new IntVar[n]; var d2 = new IntVar[n];
    for (int i = 0; i < n; i++)
    {
        d1[i] = m.NewIntVar(0, 2 * n, $"d1{i}");
        d2[i] = m.NewIntVar(-n, n, $"d2{i}");
        m.Add(d1[i] == q[i] + i);
        m.Add(d2[i] == q[i] - i);
    }
    m.AddAllDifferent(d1);
    m.AddAllDifferent(d2);
    var s = new CpSolver();
    var sw = Stopwatch.StartNew();
    var status = s.Solve(m);
    sw.Stop();
    if (status == CpSolverStatus.Optimal || status == CpSolverStatus.Feasible)
    {
        var sol = new int[n];
        for (int i = 0; i < n; i++) sol[i] = (int)s.Value(q[i]);
        return (sol, sw.Elapsed.TotalMilliseconds);
    }
    return (null, sw.Elapsed.TotalMilliseconds);
}

var (sol8, ms8) = SolveNQueensCpsat(8);
Console.WriteLine($"CP-SAT solution 8-reines : [{string.Join(", ", sol8)}]");
Console.WriteLine($"CP-SAT temps             : {ms8:F2} ms");
Console.WriteLine("Echiquier (Q = reine, . = vide) :");
for (int row = 0; row < 8; row++)
{
    var line = "";
    for (int col = 0; col < 8; col++)
        line += (sol8[col] == row ? "Q " : ". ");
    Console.WriteLine("  " + line);
}
CP-SAT solution 8-reines : [7, 3, 0, 2, 5, 1, 6, 4]
CP-SAT temps             : 20.41 ms
Echiquier (Q = reine, . = vide) :
  . . Q . . . . . 
  . . . . . Q . . 
  . . . Q . . . . 
  . Q . . . . . . 
  . . . . . . . Q 
  . . . . Q . . . 
  . . . . . . Q . 
  Q . . . . . . . 

N-Reines en CP-SAT C

Le solveur trouve une solution valide en quelques millisecondes. Deux observations pédagogiques :

  • Concision moindre qu’en MiniZinc : CP-SAT exige la création explicite des variables de diagonale intermédiaires (d1[i] = q[i]+i, d2[i] = q[i]-i) avant de pouvoir appliquer AddAllDifferent dessus. MiniZinc abstrait ces calculs via la compréhension de liste [q[i] + i | i in 1..n]. C’est le coût documenté du contrôle fin : on échange de la concision contre de la transparence sur ce que le solveur fait réellement.
  • Symétrie des solutions : pour 8 reines, il existe 92 solutions distinctes (12 à symétrie près). Le solveur en renvoie une arbitrairement — la première trouvée. L’énumération complète demanderait une boucle de recherche (« solution suivante »), hors scope de cette démonstration.

Lecture du résultat : [7, 3, 0, 2, 5, 1, 6, 4] en ~20 ms

La solution renvoyée place les 8 reines sans conflit (aucune paire ne partage une ligne ou une diagonale), résolue en ~20 ms (précision consignée par la cellule CP-SAT ci-dessus, source unique). La notation q[i] = ligne de la reine en colonne i (indexée depuis 0) : colonne 0 → ligne 7, colonne 1 → ligne 3, etc. L’échiquier ASCII permet de vérifier visuellement la validité — chaque Q est seul sur sa ligne, sa colonne et ses deux diagonales. C’est l’avantage pédagogique du rendu texte : la solution n’est pas juste un tableau de nombres abstrait, elle est relisible comme une position d’échecs.

4. Emploi du temps (timetabling)

Un problème reel : assigner creneaux et salles a 6 cours sans conflit (enseignant, salle, capacite). La contrainte C2 est une disjonction (OU) – modelisee en CP-SAT avec des booléens et OnlyEnforceIf / AddBoolOr. C’est l’apport pedagogique cles : la logique disjonctive se traduit en variables booleennes de reification.

// === Section 4 : Emploi du temps (timetabling) ===
// 6 cours, 5 creneaux, 3 salles, 3 enseignants. Contraintes C1/C2/C3.
// C1 : un enseignant ne donne pas 2 cours au meme creneau.
// C2 : une salle n'accueille qu'un cours par creneau (slot[i]!=slot[j] OU room[i]!=room[j]).
// C3 : la salle est assez grande pour les effectifs.
//
// C2 est une DISJONCTION (OU) -- en CP-SAT on la modelise avec des booleens et OnlyEnforceIf.

int nCourses = 6, nSlots = 5, nRooms = 3;
int[] teacher = { 1, 1, 2, 2, 3, 3 };
int[] students = { 30, 25, 40, 20, 35, 15 };
int[] roomCap = { 35, 45, 25 };

var mTt = new CpModel();
var slot = new IntVar[nCourses];
var room = new IntVar[nCourses];
for (int i = 0; i < nCourses; i++)
{
    slot[i] = mTt.NewIntVar(0, nSlots - 1, $"slot{i}");
    room[i] = mTt.NewIntVar(0, nRooms - 1, $"room{i}");
}

// C1 : enseignant ne donne pas 2 cours au meme creneau
for (int i = 0; i < nCourses; i++)
    for (int j = i + 1; j < nCourses; j++)
        if (teacher[i] == teacher[j])
            mTt.Add(slot[i] != slot[j]);

// C2 : une salle, un cours par creneau (disjonction reifiee)
for (int i = 0; i < nCourses; i++)
    for (int j = i + 1; j < nCourses; j++)
    {
        var bSlot = mTt.NewBoolVar($"sameSlot{i}_{j}");
        var bRoom = mTt.NewBoolVar($"sameRoom{i}_{j}");
        mTt.Add(slot[i] == slot[j]).OnlyEnforceIf(bSlot);
        mTt.Add(slot[i] != slot[j]).OnlyEnforceIf(bSlot.Not());
        mTt.Add(room[i] == room[j]).OnlyEnforceIf(bRoom);
        mTt.Add(room[i] != room[j]).OnlyEnforceIf(bRoom.Not());
        mTt.AddBoolOr(new[] { bSlot.Not(), bRoom.Not() });   // NON(meme slot ET meme room)
    }

// C3 : capacite de la salle suffisante
for (int i = 0; i < nCourses; i++)
    for (int r = 0; r < nRooms; r++)
        if (students[i] > roomCap[r])
            mTt.Add(room[i] != r);

var sTt = new CpSolver();
var stTt = sTt.Solve(mTt);
Console.WriteLine($"Emploi du temps -- status: {stTt}");
for (int i = 0; i < nCourses; i++)
    Console.WriteLine($"  Cours {i + 1} -> creneau {sTt.Value(slot[i]) + 1}, salle {sTt.Value(room[i]) + 1} (prof={teacher[i]}, {students[i]} eleves)");
Emploi du temps -- status: Optimal
  Cours 1 -> creneau 1, salle 1 (prof=1, 30 eleves)
  Cours 2 -> creneau 4, salle 3 (prof=1, 25 eleves)
  Cours 3 -> creneau 3, salle 2 (prof=2, 40 eleves)
  Cours 4 -> creneau 4, salle 2 (prof=2, 20 eleves)
  Cours 5 -> creneau 2, salle 1 (prof=3, 35 eleves)
  Cours 6 -> creneau 4, salle 1 (prof=3, 15 eleves)

Lecture du résultat : emploi du temps valide (status Optimal)

Le solveur assigne les 6 cours aux créneaux et salles en respectant les trois familles de contraintes simultanément : - C1 (enseignants) : les cours 1 et 2 (même prof 1) sont sur des créneaux distincts (1 et 4) ; idem pour 3/4 (prof 2) et 5/6 (prof 3). - C2 (salles disjonctives) : aucun couple de cours ne partage à la fois le même créneau ET la même salle. C’est ici qu’intervient la réification (OnlyEnforceIf / AddBoolOr) : la disjonction « slot différent OU salle différente » a été traduite en booléens que le solveur propage. - C3 (capacités) : le cours 3 (40 élèves) est en salle 2 (cap 45), pas en salle 1 (cap 35) ni salle 3 (cap 25) — la contrainte de capacité a été respectée.

La leçon clé : les contraintes disjonctives (OU logique), qui sont pénibles à coder impérativement, se modélisent naturellement en CP via des variables booléennes de réification. C’est l’apport spécifique de ce paradigme sur les problèmes d’ordonnancement.

5. Sudoku 4x4

Le Sudoku est l’archétype du problème de satisfaction de contraintes : une grille partiellement remplie qu’il faut compléter en respectant des règles locales. Le twin Python pilote MiniZinc depuis Python (modèle .mzn + données .dzn séparés), tandis qu’ici en CP-SAT C#, le modèle et les données vivent dans le même code. C’est une différence de style importante : - On fixe les cases initiales par des contraintes d’égalité Add(g[i,j] == valeur). - On pose AddAllDifferent sur chaque ligne, chaque colonne, et chaque bloc 2x2.

Le bloc 2x2 est la structure spécifique du Sudoku (absente du N-Reines) : il faut indexer soigneusement les coordonnées de sous-grille. C’est un bon exercice de traduction d’une règle « naturelle » (les chiffres 1-4 sans répétition dans chaque carré) en contraintes formelles que le solveur peut propager.

// === Section 5 : Sudoku 4x4 pilote depuis le code ===
// Le twin Python pilote MiniZinc depuis Python (model + data .dzn).
// En CP-SAT C#, le modele et les donnees vivent dans le meme code : on fixe les
// cases initiales par des contraintes d'egalite, puis alldifferent sur lignes/colonnes/blocs.

int[,] initSudoku = { { 0, 0, 3, 0 }, { 3, 0, 0, 2 }, { 0, 3, 0, 0 }, { 0, 0, 2, 0 } };
Console.WriteLine("Grille initiale (0 = vide) :");
for (int i = 0; i < 4; i++)
    Console.WriteLine("  " + string.Join(" ", Enumerable.Range(0, 4).Select(j => initSudoku[i, j] > 0 ? initSudoku[i, j].ToString() : ".")));

var mSk = new CpModel();
var g = new IntVar[4, 4];
for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
    {
        g[i, j] = mSk.NewIntVar(1, 4, $"g{i}{j}");
        if (initSudoku[i, j] > 0) mSk.Add(g[i, j] == initSudoku[i, j]);
    }
for (int i = 0; i < 4; i++)
{
    mSk.AddAllDifferent(new[] { g[i, 0], g[i, 1], g[i, 2], g[i, 3] });   // ligne i
    mSk.AddAllDifferent(new[] { g[0, i], g[1, i], g[2, i], g[3, i] });   // colonne i
}
for (int br = 0; br < 2; br++)   // blocs 2x2
    for (int bc = 0; bc < 2; bc++)
        mSk.AddAllDifferent(new[] { g[br * 2, bc * 2], g[br * 2, bc * 2 + 1], g[br * 2 + 1, bc * 2], g[br * 2 + 1, bc * 2 + 1] });

var sSk = new CpSolver();
sSk.Solve(mSk);
Console.WriteLine("\nSolution CP-SAT :");
for (int i = 0; i < 4; i++)
    Console.WriteLine("  " + string.Join(" ", Enumerable.Range(0, 4).Select(j => sSk.Value(g[i, j]).ToString())));
Console.WriteLine("\nVoir la serie Sudoku pour des solveurs CSP complets -> ../../Sudoku/README.md");
Grille initiale (0 = vide) :
  . . 3 .
  3 . . 2
  . 3 . .
  . . 2 .

Solution CP-SAT :
  4 2 3 1
  3 1 4 2
  2 3 1 4
  1 4 2 3

Voir la serie Sudoku pour des solveurs CSP complets -> ../../Sudoku/README.md

6. Comparaison : imperatif vs declaratif

On resout N-Reines des deux facons pour comparer. Le backtracking pur (imperatif) est plus lent et moins declaratif ; CP-SAT (declaratif) delegue au solveur. La troisieme approche du twin Python (MiniZinc) est omise ici (pas de binding .NET) – CP-SAT la represente dans le paradigme declaratif.

Tableau comparatif

// === Section 6 : Comparaison imperatif vs declaratif ===
// N-Reines resolu de 2 facons : (1) backtracking pur (imperatif), (2) CP-SAT (declaratif).
// (MiniZinc, 3e approche du twin Python, est remplace par CP-SAT ici -- meme paradigme declaratif.)

static int[] SolveNQueensBacktrack(int n)
{
    int[] queens = new int[n];
    bool IsSafe(int col, int row)
    {
        for (int c = 0; c < col; c++)
            if (queens[c] == row || Math.Abs(c - col) == Math.Abs(queens[c] - row)) return false;
        return true;
    }
    bool Solve(int col)
    {
        if (col == n) return true;
        for (int row = 0; row < n; row++)
            if (IsSafe(col, row)) { queens[col] = row; if (Solve(col + 1)) return true; }
        return false;
    }
    return Solve(0) ? queens : null;
}

int nBench = 12;
var swBt = Stopwatch.StartNew();
var solBt = SolveNQueensBacktrack(nBench);
swBt.Stop();
Console.WriteLine($"Backtracking pur : {swBt.Elapsed.TotalMilliseconds,8:F2} ms  -> solution trouvee: {solBt != null}");

var (solCpsat12, msCpsat12) = SolveNQueensCpsat(nBench);
Console.WriteLine($"CP-SAT declaratif: {msCpsat12,8:F2} ms  -> solution trouvee: {solCpsat12 != null}");
Console.WriteLine($"\n{nBench}-Reines : les deux approches trouvent une solution valide.");
Backtracking pur :     0.43 ms  -> solution trouvee: True
CP-SAT declaratif:    20.11 ms  -> solution trouvee: True

12-Reines : les deux approches trouvent une solution valide.

Lecture du résultat : surprise — le backtracking gagne sur 12 reines

Résultat contre-intuitif : sur 12 reines, le backtracking pur (~0,4 ms) bat CP-SAT (~20 ms) — par un facteur ~50 (runtimes précis consignés par la cellule de comparaison ci-dessus, source unique) ! Pourquoi le moteur « industriel » perd-il ici ? Parce que le solveur déclaratif paie un coût de setup constant : construction du modèle, création des variables intermédiaires, initialisation de la recherche CP. Sur une instance petite et peu contrainte, ce surcoût n’est pas rentabilisé.

C’est une leçon d’humilité importante : CP-SAT n’est pas magiquement plus rapide. Son avantage émerge sur des instances plus larges ou plus contraintes, là où le backtracking naïf souffre de son manque de propagation (il explore des branches que la cohérence d’arc coupe immédiatement en CP). Le choix entre paradigmes n’est pas une question de dogme mais de régime : petit/proche de la racine → backtracking ; grand/fortement contraint → solveur déclaratif. Le tableau comparatif ci-après généralise cette analyse sur sept critères.

Sur cette petite instance (12 reines), le backtracking pur est plus rapide que CP-SAT : le solveur déclaratif paie un coût de setup constant (construction du modèle, variables intermédiaires, initialisation de la recherche CP). Ce coût est rentabilisé sur des instances plus contraintes ou plus larges, là où le backtracking naïf souffre de son manque de propagation (il explore des branches que la cohérence d’arc coupe immédiatement en CP). Le tableau ci-dessous compare les trois paradigmes — Python pur, CP-SAT, MiniZinc — sur sept critères.

// Tableau comparatif des 3 paradigmes (Python pur / CP-SAT / MiniZinc).
// Repris du twin Python, avec la colonne MiniZinc (high-level, multi-solveur).
var rows = new[]
{
    new { Crit = "Lisibilite mathematique", Py = "Faible", Cp = "Moyenne", Mz = "Haute" },
    new { Crit = "Flexibilite du solveur", Py = "Aucune (ad-hoc)", Cp = "CP-SAT uniquement", Mz = "Multi-solveur" },
    new { Crit = "Integration Python/.NET", Py = "Totale", Cp = "Excellente (API native)", Mz = "Via package/CLI" },
    new { Crit = "Separation modele/donnees", Py = "Non", Cp = "Non (code)", Mz = "Oui (.mzn + .dzn)" },
    new { Crit = "Performance brute", Py = "Lente", Cp = "Tres rapide", Mz = "Depend du solveur" },
    new { Crit = "Cas d'usage ideal", Py = "Prototypage", Cp = "Production", Mz = "Modelisation, recherche" },
};
string sep = new string('-', 86);
Console.WriteLine("Comparaison des paradigmes de resolution\n" + new string('=', 86));
Console.WriteLine($"{"Critere",-30} {"Python pur",-18} {"CP-SAT",-18} {"MiniZinc",-18}");
Console.WriteLine(sep);
foreach (var r in rows)
    Console.WriteLine($"{r.Crit,-30} {r.Py,-18} {r.Cp,-18} {r.Mz,-18}");
Console.WriteLine(new string('=', 86));
Comparaison des paradigmes de resolution
======================================================================================
Critere                        Python pur         CP-SAT             MiniZinc          
--------------------------------------------------------------------------------------
Lisibilite mathematique        Faible             Moyenne            Haute             
Flexibilite du solveur         Aucune (ad-hoc)    CP-SAT uniquement  Multi-solveur     
Integration Python/.NET        Totale             Excellente (API native) Via package/CLI   
Separation modele/donnees      Non                Non (code)         Oui (.mzn + .dzn) 
Performance brute              Lente              Tres rapide        Depend du solveur 
Cas d'usage ideal              Prototypage        Production         Modelisation, recherche
======================================================================================
#load "../../../Probas/Infer/SvgChartHelper.cs"
// Visualisation SVG statique (remplace Plotly-CDN qui rend BLANC en consultation statique --
// GitHub sandbox les <script>; EPIC #3801 Prong A + EPIC #6927). Technique #6942 : <svg> inline
// via SvgChartHelper (zero-dependance, rend sur GitHub/nbviewer/offline, aucun CDN ni <script>).

// (1) Concision du modele : lignes de code estimees, 3 solveurs groupes par famille de probleme.
var problems = new[] { "N-Reines", "Emploi du temps", "Sudoku" };
display(SvgChartHelper.GroupedBar(
    "Concision du modele (lignes de code estimees)",
    problems,
    new double[][] { new double[]{20,60,50}, new double[]{12,45,35}, new double[]{5,25,20} },
    new[] { "Python pur", "CP-SAT", "MiniZinc" },
    width: 720, height: 340));

// (2) Evaluation multi-criteres (score 1-5) : 3 solveurs groupes par critere.
var criteria = new[] { "Concision", "Lisibilite", "Portabilite", "Integr. .NET", "Performance" };
display(SvgChartHelper.GroupedBar(
    "Evaluation multi-criteres (score 1-5)",
    criteria,
    new double[][] { new double[]{2,2,1,5,2}, new double[]{3,3,2,5,5}, new double[]{5,5,5,3,4} },
    new[] { "Python pur", "CP-SAT", "MiniZinc" },
    width: 720, height: 340))
Concision du modele (lignes de code estimees)016.232.448.664.8N-ReinesEmploi du tempsSudokuPython purCP-SATMiniZinc
Evaluation multi-criteres (score 1-5)01.352.74.055.4ConcisionLisibilitePortabiliteIntegr. .NETPerformancePython purCP-SATMiniZinc
// === Section 7 : Exercices (stubs C.1 -- a completer par l'etudiant) ===
// Exercice 1 : Carre magique 3x3. Les entiers 1..9 tous differents, chaque ligne/colonne/
// diagonale sommant a magic_sum = n*(n*n+1)/2 = 15 pour n=3. modele CP-SAT a completer.
static int[][] MagicSquareCpsat()   // TODO etudiant
{
    // Indice : 9 IntVar(1,9), AddAllDifferent sur les 9, puis Add(==15) sur chaque ligne,
    // colonne et les 2 diagonales. Retourner la grille 3x3 resolue, ou null.
    // Etape 1 : creer le modele et les 9 variables.
    // Etape 2 : ajouter AddAllDifferent et les contraintes de somme (magic_sum = 15).
    // Etape 3 : resoudre et extraire la grille.
    Console.WriteLine("Exercice a completer : carre magique 3x3 en CP-SAT");
    return null;
}
MagicSquareCpsat();
Exercice a completer : carre magique 3x3 en CP-SAT
// Exercice 2 : Coloration de graphe. Graphe A-B-C, A-D, B-E, C-F, D-E, E-F.
// Minimiser le nombre de couleurs (chromatique). modele CP-SAT a completer.
static int[] GraphColoringCpsat()   // TODO etudiant
{
    // Indice : 6 IntVar(0, maxColors-1) pour les sommets A..F. Pour chaque arete (u,v),
    // Add(color[u] != color[v]). Minimiser le nombre de couleurs distinctes (variable objectif).
    // Etape 1 : modeliser les contraintes d'aretes (voisins de couleur differente).
    // Etape 2 : chercher le minimum de couleurs (essayer k=1, 2, 3... jusqu'a faisabilite).
    Console.WriteLine("Exercice a completer : coloration de graphe en CP-SAT");
    return null;
}
GraphColoringCpsat();
Exercice a completer : coloration de graphe en CP-SAT
// Exercice 3 : VRP simplifie (6 noeuds, 1 depot). Minimiser la distance totale
// d'une tournee passant par tous les clients. modele CP-SAT a completer.
static int[] VrpCpsat()   // TODO etudiant
{
    // Indice : variables next[i] = successeur du noeud i. AddAllDifferent(next) pour une
    // permutation, contrainte de circuit (cycle hamiltonien), minimiser sum dist[i,next[i]].
    // Etape 1 : fixer les variables next et la permutation.
    // Etape 2 : ajouter l'elimination de sous-tours (ou utiliser AddCircuit si disponible).
    // Etape 3 : minimiser la distance totale.
    Console.WriteLine("Exercice a completer : VRP simplifie en CP-SAT");
    return null;
}
VrpCpsat();

Console.WriteLine("\n=== Recapitulatif ===");
Console.WriteLine("OR-Tools CP-SAT est le moteur declaratif CP de reference en .NET (Prong A #3801).");
Console.WriteLine("Il exprime les memes modeles que MiniZinc (alldifferent, sommes, optimisation)");
Console.WriteLine("avec une API imperative. MiniZinc reste superieur pour la concision et le");
Console.WriteLine("multi-solveur (Gecode, Chuffed, CBC...). Les deux partagent le paradigme :");
Console.WriteLine("declarer les contraintes, laisser le solveur chercher -- l'oppose du backtracking.");
Exercice a completer : VRP simplifie en CP-SAT

=== Recapitulatif ===
OR-Tools CP-SAT est le moteur declaratif CP de reference en .NET (Prong A #3801).
Il exprime les memes modeles que MiniZinc (alldifferent, sommes, optimisation)
avec une API imperative. MiniZinc reste superieur pour la concision et le
multi-solveur (Gecode, Chuffed, CBC...). Les deux partagent le paradigme :
declarer les contraintes, laisser le solveur chercher -- l'oppose du backtracking.

Visualisation ASCII (concision et score multi-critères)

Les graphiques matplotlib du twin Python (qui dispose d’une bibliothèque graphique native) sont remplacés ici par un rendu SVG statique autonome via SvgChartHelper — technique zero-dépendance qui rend correctement sur GitHub, nbviewer et en consultation offline (les CDN Plotly rendent blanc en sandbox statique, EPIC #3801/#6927). Deux axes de comparaison entre les trois paradigmes (Python pur / CP-SAT / MiniZinc) : - Concision du modèle (lignes de code estimées) par famille de problème. - Évaluation multi-critères (score 1-5) : concision, lisibilité, portabilité, intégration .NET, performance.

Ces visualisations résument synthétiquement le compromis fondamental : MiniZinc domine en concision et lisibilité (langage dédié), CP-SAT en performance et intégration .NET (moteur industriel), tandis que le Python pur reste un outil de prototypage rapide sans la puissance de propagation d’un vrai solveur.


Synthèse : ce qu’il faut retenir

Ce notebook a parcouru le changement de paradigme qu’opère la programmation par contraintes : au lieu de coder comment explorer l’espace de recherche (backtracking impératif), on déclare ce que la solution doit satisfaire, et on délègue l’exploration au solveur OR-Tools CP-SAT.

Les motifs d’API CP-SAT rencontrés

Catégorie Méthode CP-SAT C# Problème illustré
Variable de décision model.NewIntVar(bornes) Système d’équations, monnaie
Contrainte arithmétique AddMultiplicationEquality Équations non-linéaires
Contrainte globale AddAllDifferent N-Reines, Sudoku 4×4
Réification (disjonction) OnlyEnforceIf / AddBoolOr Emploi du temps (OU logique)
Optimisation model.Minimize(objectif) Problème de monnaie (preuve d’optimalité)

Le point clé : la réification

Le saut conceptuel le plus important est la réification — traduire une disjonction (“le cours A ou le cours B occupe la salle”) en variables booléennes pilotées par OnlyEnforceIf. C’est l’apport spécifique de la CP : exprimer une logique non-linéaire de façon que le solveur puisse la propager.

Déclaratif vs impératif

La section comparative l’a montré empiriquement : le backtracking pur (impératif) reste correct mais redonne au programmeur le fardeau de l’exploration, tandis que CP-SAT (déclaratif) prouve l’optimalité — aucune meilleure solution n’existe — sans qu’on écrive la moindre heuristique de parcours. C’est cette garantie de complétude qui distingue un solveur industriel d’une métaheuristique.

Compromis concision vs contrôle

Le coût de cette puissance : CP-SAT C# est plus verbeux que MiniZinc (création explicite des variables intermédiaires, des diagonales de N-Reines, des booléens de réification). C’est le ** compromis documenté** de ce twin : MiniZinc privilégie la concision déclarative, CP-SAT C# le contrôle fin et l’intégration .NET native. Les deux convergent vers la même idée — séparer la modélisation de la résolution.


Références

Ce notebook enseigne la modélisation déclarative par contraintes via le langage MiniZinc, résolu côté C# par le solveur OR-Tools CP-SAT (NuGet Google.OrTools). Les deux références canoniques ci-dessous ancrent cette démarche dans la littérature fondatrice et l’écosystème standard du domaine.

  • Nethercote, N., Stuckey, P. J., Becket, R., Brand, S., Duck, G. J., & Tack, G. (2007). « MiniZinc: Towards a Standard CP Modelling Language. » In Principles and Practice of Constraint Programming (CP 2007), LNCS 4741, p. 529-543. Springer. DOI : 10.1007/978-3-540-74970-7_38. — l’article fondateur qui définit MiniZinc comme langage de modélisation standard pour la programmation par contraintes, indépendant des solveurs sous-jacents. Cartographie directe avec la philosophie déclarative, la syntaxe (variables, domaines, contraintes, objectifs) et les contraintes globales (alldifferent, circuit) du notebook.

  • MiniZinc Team. The MiniZinc Handbook (spécification du langage + interface vers les solveurs) et la MiniZinc Challenge (compétition annuelle de solveurs CP, depuis 2008). www.minizinc.org. — la spécification en évolution du langage et le banc d’essai canonique qui évalue les solveurs backend (dont OR-Tools CP-SAT, invoqué ici via le NuGet Google.OrTools).

Fidélité à la source : ce notebook (jumeau C# du App-8-MiniZinc.ipynb Python) explicite et met en pratique la modélisation déclarative que Nethercote et al. (2007) ont formalisée. Le socle théorique (variables / domaines / contraintes / objectifs) suit la spécification MiniZinc ; la résolution s’appuie sur OR-Tools CP-SAT, l’un des solveurs backend que la MiniZinc Challenge évalue chaque année.

Retour au sommet