Consignes
Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant un répertoire TP1-NOM avec :
-
votre fichier naivesat-NOM.py modifié
-
vos fichier question2-NOM.sat, cartes-NOM.sat et xor3-NOM.sat
-
vos fichiers addition-NOM.py multiplication-NOM.py et zebra-NOM.py
-
un fichier texte README contenant les réponses aux questions du TP.
Vous aurez compris que NOM devra être remplacé par votre nom (ou vos noms en cas de binomes) !
Tous vos fichiers doivent comporter une entête avec votre nom et votre groupe de TP.
Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note !
Liens utiles
-
le solveur naivesat-NOM.py (Python), à compléter
-
le générateur de clauses pour le problème de Einstein (Python), à compléter
-
le générateur de clauses pour l'addition en binaire (Python), à compléter
-
le générateur de clauses pour la multiplication en binaire (Python), à compléter
Préliminaires : naivesat.py, un solveur récursif naïf en Python
Cette première séance de travaux pratiques doit vous aidez à vous familiariser avec le problème de la satisfiabilité : SAT.
Étant donnée une formule (données en forme normale conjonctive), comme (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4) nous voulons savoir s'il est possible de satisfaire cette formule (càd la rendre vraie) en choisissant des valeurs booléennes pour x1, x2, x3 et x4.
Le problème de la satisfiabilité fera également l'objet du TP 2, où il faudra améliorer un solveur écrit en C.
Le format 'SAT' de D. Knuth pour les formules en forme normale conjonctive
Les formules considérées seront toujours en forme normale conjonctive. Elle seront en général données dans des fichiers de type 'SAT' (extension .sat).
-
Chaque ligne qui commence par "~␣" (␣ représente un espace) est ignorée : c'est un commentaire.
-
Les lignes vides sont ignorées.
-
Chaque autre ligne contient une clause, donnée par la liste de ces littéraux :
-
un littéral négatif commence par le signe "~", suivi d'une chaine représentant la variable correspondante,
-
un littéral positif ne contient que la variable correspondante.
Même si ce n'est pas obligatoire, il est prudent de limiter vos noms de variables à des chaines ne contenant que des caractères ASCII alphanumériques (avec
(),:;_@
). -
Voici par exemple la représentation de la formule (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4) :
~ un exemple de formule toute simple dans un fichier SAT
x1 ~x2 x3
~x1 ~x3 x4
x2 ~x4
Utilisation de naivesat
Le fichier naivesat-NOM.py contient un solveur SAT récursif naïf écrit en Python.
Vous devrez le compléter (question 1) pour le rendre fonctionnel, mais voici comment il s'utilise pour chercher une solution
$ ./naivesat-NOM.py FICHIER_FORMULE
où FICHIER_FORMULE est le fichier (au format SAT) qui contient la formule à considérer
Par exemple :
$ ./naivesat-NOM.py test2.sat
SATISFIABLE
~a b c d
$ ./naivesat-NOM.py test3.sat
UNSATISFIABLE
Dans le premier cas, la formule est satisfiable avec les valeurs a=0, b=c=d=1, alors que dans le second cas, la formule n'est pas satisfiable.
Deux autres fonctionnalités pratiques de naivesat-NOM.py sont :
-
Si vous donnez l'option -T ou --test à naivesate.py, votre fonction
test
sera appelée avec la formule et le reste des arguments de la ligne de commande. C'est pratique ... pour faire quelques tests. -
l'option --help ou -h vous affiche un petit message d'aide.
-
Si vous n'êtes pas sous un environnement de type Unix avec un shell, il est possible de passer d'exécuter un script avec des arguments en utilisant une version récente (≥ 3.7.4) de Idle (menu Run -> Customized Run) ou avec l'IDE Spyder (menu Run -> Configuration per file ..., en sélectionnant Execute in a dedicated console et Command line options).
Code du solveur
En interne, les formules traitées par naivesat-NOM.py sont des listes de listes d'entiers (non nuls).
-
Toutes les variables sont de la forme xk (k>0) avec
-
le littéral xk représenté par l'entier k,
-
le littéral ¬xk représenté par l'entier -k.
-
Chaque clause est représentée par la liste de ses littéraux. Par exemple,
x3 ∨ ¬x7 ∨ x1 sera représentée par la liste
[3, -7, 1]
. -
-
la conjonction de toutes les clauses est représentée par la liste des clauses. Par exemple, la formule
(x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4)est représentée par la liste
[[1, -2, 3], [-1, -3, 4], [2, -4]]
La première fonction définie dans naivesat-NOM.py est la fonction solve
.
Cette fonction ne fait que quelques lignes et les commentaires devraient vous
aidez à comprendre comment elle fonctionne.
La deuxième fonction, utilisée par la fonction solve
est
simplify_formula
. Cette fonction prend en argument une formule
(càd une liste de listes d'entiers, voir plus haut) et un littéral (càd un
entier non nul). Cette fonction doit simplifier la formule lorsqu'on suppose
que le littéral fourni est vrai.
Par exemple, si on suppose que le littéral x2 est vrai :
avant simplification : | (x1 | ∨ | ¬x2 | ∨ | x3) | ∧ | (¬x1 | ∨ | ¬x3 | ∨ | x4) | ∧ | (x2 | ∨ | ¬x4) |
après simplification : | (x1 | ∨ | x3) | ∧ | (¬x1 | ∨ | ¬x3 | ∨ | x4) |
En effet,
-
¬x2 est faux, et on a (x1 ∨ FAUX ∨ x3) = (x1 ∨ x3)
-
x2 est vrai, et comme (VRAI ∨ ¬x4) = VRAI, on a ... ∧ ... ∧ (VRAI ∨ ¬x4) = ... ∧ ... ∧ VRAI = ... ∧ ....
La fonction simplify_formula
doit reconstruire une liste
de clauses à partir de la liste de clauses F
:
-
en omettant les clauses qui contiennent
lit
, -
en remplaçant les clauses qui contiennent
-lit
par la même clause, sans-lit
, -
en laissant les autres clauses inchangées. -
ATTENTION, il ne faut pas modifier la formule ou les clauses,
mais en recréer de nouvelles. C'est pour cela qu'on crée une nouvelle formule
simplified_F
et qu'il faudra recopier tous les littéraux de
chaque clause pour en créer de nouvelles.
Vous ne devez par exemple pas utiliser la méthode
des listes Python !
remove
Je vous rappelle également que la bonne manière d'insérer un élément
dans une liste est d'utiliser la méthode append
. N'utilisez pas
!
L = L+[element]
-
Complétez le corps de la fonction
simplify_formula
et testez la. Vous pouvez pour cela utiliser l'option -T ou --test pour lancer la fonction de tests :$ ./naivesat-NOM.py test.sat --test 1 simplification avec 1 avant : [[1, 2, -3], [2, 3, -4], [3, 4, 1], [4, -1, 2], [-1, -2, 3], [-2, -3, 4], [-3, -4, -1], [-4, 1, -2]] après : [[2, 3, -4], [4, 2], [-2, 3], [-2, -3, 4], [-3, -4]]
N'hésitez pas à modifier la fonction de tests.
-
Testez le solveur sur la formule précédente (x1 ∨ ¬x2 ∨ x3) ∧ (¬x1 ∨ ¬x3 ∨ x4) ∧ (x2 ∨ ¬x4).
Le résultat est il bien correct ?
Attention, pour donner un argument négatif, il faut ajouter "--" après --test ou -T. Sinon, le -1 est interprété comme une option de la commande !
$ ./naivesat-NOM.py test.sat --test -- -1
simplification avec 1
avant : [[1, 2, -3], [2, 3, -4], [3, 4, 1], [4, -1, 2], [-1, -2, 3], [-2, -3, 4], [-3, -4, -1], [-4, 1, -2]]
après : [[2, -3], [2, 3, -4], [3, 4], [-2, -3, 4], [-4, -2]]
-
La formule (a ∨ ¬b) ∧ (¬b ∨ ¬c) ∧ (¬a ∨ ¬c) ∧ (¬a ∨ ¬b ∨ c) ∧ (a ∨ b ∨ ¬c) est elle satisfiable ?
Créez un fichier question2-NOM.sat que vous joindrez à votre archive pour tester.
-
Y'a t'il d'autres solution que celle trouvée ? Comment procédez vous pour les trouver ? (Indice: il faudra rajouter des clauses pour interdire la solution trouvée)
Pour créer un fichier .sat, vous pouvez utiliser n'importe quel éditeur de texte : SublimeText, Gedit, nano, etc.
Rechercher toutes les solutions
Pour les petites formules, il est parfois intéressant de chercher toutes les solutions à un problème.
-
En partant du code de la fonction
solve
, écrivez une fonctionall_solutions
qui affiche toutes les solutions, au fur et à mesure qu'elle les trouve.Cette fonction aura exactement le même "squelette" que
solve
, mais :-
elle ne renvoie pas de solution, elle les affiche avec la fonction
print_sol
, -
elle explore toujours les deux alternative "
+lit
" et "-lit
".
Il ne sera donc plus possible d'utiliser le
or
du Python qui ne calcule pas la seconde alternative. -
-
Essayez (sans y passer trop de temps) de renvoyer le nombre de solutions trouvées.
Vous pouvez appeler la fonction all_solutions
sur un fichier
avec l'option -A ou --all de naivesat-NOM.py.
Testez plusieurs fois naivesat-NOM.py avec l'option --all sur la formule précédente) en mélangeant aléatoirement la formule (option -r / --randomize sur la ligne de commande).
Que constatez vous ? Proposez une explication.
1. Quelques problèmes de satisfiabilité
Il est possible d'encoder de nombreux problèmes comme des problèmes de satisfiabilité. Par exemple, les règles d'un sudoku s'expriment comme des contraintes logiques :
-
on dispose 9 × 9 × 9 = 729 variables : x1,1,1 pour dire que "la case (1,1) contient le chiffre 1", x1,1,2 pour dire que "la case (1,1) contient le chiffre 2", ..., x9,9,9 pour dire que "la case (9,9) contient le chiffre 9",
-
la contrainte "chaque ligne contient au moins un 1" s'exprime comme x1,1,1 ∨ x2,1,1 ∨ ... ∨ x9,1,1, etc.
-
on code également les contraintes la ligne k contient au plus un 1,
-
on fait pareil pour les diagonales et les "petits carrés",
-
sans oublier d'autres contraintes comme chaque case contient exactement 1 chiffre,
-
...
Bien sûr, il faut utiliser un programme pour générer une telle formule !
Écrivez un programme qui génère un fichier SAT à partir d'une grille de sudoku, et qui affiche la grille obtenu à partir de la solution trouvée par le solveur.
Attention, ceci se rapproche plutôt d'un mini projet que d'une simple question. Ne commencer à y réfléchir qu'une fois que vous avez fait le reste du TP !
(Pour information, mon fichier Python qui permet de faire ceci fait un peu moins de 150 lignes.)
1.1. L'énigme de Einstein
Comme naivesat-NOM.py ne peut pas (encore) gérer un tel nombre de variables / clauses (voici un exemple de fichier SAT correspondant à une grille de sudoku), nous allons regarder un autre problème "amusant" : une variante de l'énigme d'Einstein.
échauffement
Ce problème est extrait du lien suivant : http://villemin.gerard.free.fr/LogForm/Integram.htm.
On suppose que les 4 cartes suivantes sont les 4 as :
1 | 2 | 3 | 4 |
Et on dispose des indices suivants :
-
En 2, il n'y a ni l'as de trèfle, ni l'as de pique.
-
L'as de trèfle est plus à droite que l'as de carreau.
-
L'as de carreau et l'as de cœur ne sont pas à côté l'un de l'autre.
Question : quelles sont les positions des as ? La solution est elle unique ?
-
Écrivez (à la main) un fichier SAT pour résoudre le problème.
-
il y aura 16 variables :
-
X_1_trefle
pour l'as de trèfle est en position 1 -
X_2_trefle
pour l'as de trèfle est en position 2 -
...
-
X_3_coeur
pour l'as de coeur est en position 3 -
...
-
-
le premier indice donne 2 clauses ne contenant qu'un littéral
-
le deuxième indice donne 4 clauses qu'il vous faudra transformer en FNC (c'est facile) :
-
X_4_trefle ⟶ X_1_carreau ∨ X_2_carreau ∨ X_3_carreau
-
X_3_trefle ⟶ X_1_carreau ∨ X_2_carreau
-
X_2_trefle ⟶ X_1_carreau
-
¬X_1_trefle
(en effet, l'as de trèfle ne peut pas se trouver en position 1)
-
-
le troisième indice donne 6 clauses de 2 littéraux chacune. Une de ces clauses est
¬(X_2_carreau ∧ X_3_coeur)
. (Là encore, la transformation en FNC est très facile.)
Il y a souvent plusieurs choix possibles pour exprimer les contraintes. Formellement, l'indice 1 s'exprime par les 2 clauses
(~X_2_trefle) ∧ (~X_2_pique)
, mais la clause(X_2_coeur ∨ X_2_carreau)
pourrait aussi être utilisée.Cette deuxième possibilité est en une conséquence de l'indice 1 et de la contrainte logique
(X_2_pique ∨ X_2_coeur ∨ X_2_carreau ∨ X_2_carreau)
. Autrement dit, interpréter l'indice par cette clause nécessite une étape de raisonnement de la part de l'utilisateur, alors que la première solution est directe.Il vous faudra aussi ajouter d'autres contraintes. La plus complexe sera pour dire, par exemple, qu'il n'y a qu'un seul as de pique. Les clauses correspondantes, au format SAT, sont :
~ il y a au plus un as de pique ~X_1_pique ~X_2_pique ~X_1_pique ~X_3_pique ~X_1_pique ~X_4_pique ~X_2_pique ~X_3_pique ~X_2_pique ~X_4_pique ~X_3_pique ~X_4_pique
Avec une utilisation judicieuse du copier/coller, l'écriture de ce fichier ne devrait pas vous prendre trop de temps. Je vous conseille également de mettre des commentaires pour expliquer à quoi correspond chaque groupe de clauses.
-
-
Cherchez une solution au problème avec naivesat-NOM.py, et vérifiez qu'elle est bien cohérente avec les indices.
Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.
Le problème de Einstein
Voici maintenant une variante de l'énigme d'Einstein.
On suppose que 5 maisons sont alignée, en positions 1, 2, 3, 4, 5:
1 | 2 | 3 | 4 | 5 |
Chaque maison est habitée par une personne :
-
qui a un animal
-
qui boit une boisson particulière
-
qui écoute un style de musique
-
qui exerce un métier
On sait que
-
Le musicien a une tortue.
-
Le propriétaire de la maison jaune écoute du jazz.
-
La personne qui écoute du hard-rock a un chat.
-
La maison en bois se trouve juste à gauche de la maison blanche.
-
Le chercheur habite dans la maison en briques.
-
Le footballeur habite juste à coté de l'immeuble.
-
La personne qui écoute de la musique classique est voisine de celle qui boit de la bière.
-
La personne qui écoute du hiphop boit du thé.
-
La personne qui habite dans la maison du milieu boit beaucoup de soda.
-
Le footballeur habite dans la première maison.
-
La personne qui écoute de la musique classique est voisine de la personne qui a des poissons.
-
Le boulanger écoute de la chanson française.
-
La personne qui a un hamster est voisine de celle qui écoute du jazz.
-
L'architecte boit de l'eau.
-
Le propriétaire de la maison en bois aime bien le whisky.
Question : qui est le propriétaire du zèbre ?
L'expression de ce problème comme problème de satisfiabilité utilisera 125 variables ! Il est donc préférable de générer la formule par un programme.
Les variables
Les variables seront toutes de la forme E_i_a
pour dire
"l'attribut a est à l'Emplacement i"
Les attributs sont les types de maisons (5 possibilités), animaux (5 possibilités), boissons (5 possibilités), styles de musique (5 possibilité), métiers (5 possibilités). Il y a donc 25 attributs et 5 emplacements possibles. Cela fait bien un total de 25 × 5 = 125 variables.
Par exemple, la formule E_3_musicien ∧ E_3_brique indique que le musicien habite dans la maison en briques, et que cette maison est en position 3.
Les indices
La plupart des indices sont de la forme "les attributs a et b sont au même endroit". Par exemple, le premier indice est équivalent à "le musicien et la tortue sont au même endroit".
On peut exprimer ceci par la formule
(E_1_musicien ⟶ E_1_tortue) | ∧ |
(E_2_musicien ⟶ E_2_tortue) | ∧ |
(E_3_musicien ⟶ E_3_tortue) | ∧ |
(E_4_musicien ⟶ E_4_tortue) | ∧ |
(E_5_musicien ⟶ E_5_tortue) |
qu'il vous faudra transformer en FNC (5 clauses de 2 littéraux).
Nous allons utiliser 4 types d'indices :
-
"les attributs a et b sont au même endroit", généré par l'appel à la fonction Python
implies(a, b)
; -
"l'attribut a se trouve juste à gauche de l'attribut b", généré par la fonction
left_of(a, b)
; -
"l'attribut a est voisin de l'attribut b", généré par la fonction
next_to(a, b)
; -
"l'attribut a est à l'emplacement i", généré par la fonction
exact(i, a)
.
Les fonctions left_of
, next_to
et
exact
sont déjà définies dans le fichier zebra-NOM.py.
Les contraintes
Il faut à ceci ajouter 3 autres contraintes logiques :
-
"tous les attributs sont à un emplacement" :
(E_1_jazz ∨ E_2_jazz ∨ E_3_jazz ∨ E_4_jazz ∨ E_5_jazz) ∧ (E_1_whisky ∨ E_2_whisky ∨ E_3_whisky ∨ E_4_whisky ∨ E_5_whisky) ∧ ............. etc. ............. générée par la fonction
constraints_1()
-
"aucun attribut n'est à 2 emplacements différents" générée par la fonction
constraints_2()
-
"il n'y a pas 2 attributs d'une même catégorie au même emplacement" générée par la fonction
constraints_3()
Les fonctions constraints_2
et constraints_3
sont
déjà définies dans le fichier zebra-NOM.py.
-
Récupérez le fichier zebra-NOM.py et
-
écrivez le corps de la fonction
implies
, -
écrivez le corps de la fonction
constraints_1
, -
complétez avec les appels pertinents pour chacun des indices.
Ce programme, lorsqu'on le lance avec
$ ./zebra-NOM.py ...
affiche à l'écran la formule CNF correspondant au problème de Einstein.
-
-
lancez naivesat-NOM.py sur la formule obtenue.
Vous pouvez sauvegarder la formule générée par le programme zebra-NOM.py dans un fichier avec
$ ./zebra-NOM.py > formule-zebre.sat
et utiliser naivesat-NOM.py comme précédemment, "brancher" les 2 programmes zebra-NOM.py et naivesat-NOM.py avec
$ ./zebra-NOM.py | ./naivesat-NOM.py
-
Répondez à la question "Qui est le propriétaire du zèbre ?".
L'ordre des clauses d'une formule est parfois important.
Essayer de résoudre le problème en mélangeant la formule (option -r / --random), ou
simplement en mettant les contraintes logiques (appels aux fonctions
constraints_1
, constraints_2
et
constraints_3
) avant les indices pour constater une grosse
différence de temps de calcul !
Normalement, il n'y a qu'une seule solution au problème ! Si vous lancez naivesat-NOM.py avec l'option -A / --all, vous devriez obtenir 2 solutions. (Le propriétaire du zèbre est le même dans les deux.)
Essayez de comparer les solutions afin de trouver le bug que j'ai introduit dans le fichier zebra-NOM.py.
1.2. Arithmétique
Étant donné un circuit booléen, il est facile de calculer la sortie à partir des entrées. Avec un solveur, on peut essayer de faire l'inverse : étant donné le résultat, trouver une entrée correspondante !
Les entiers d'un ordinateur sont simplement des suites de (32 ou 64) bits. Les opérations sont faites par des circuits, c'est à dire des opérations logiques sur ces bits. Nous allons essayer d'inverser les circuits correspondants à des opérations arithmétique.
Inverser l'addition
Par exemple, l'addition de 2 nombres écrits en binaire se fait en additionnant leurs bits avec la retenue colonne par colonne.
Pour additionner les nombres ak ... a1 a0 et bk ... b1 b0, on peut "poser l'addition"
cn | .......... | c2 | c1 | c0 | (retenues) | |||||
an | .......... | a2 | a1 | a0 | (1er nombre) | |||||
+ | bn | .......... | b2 | b1 | b0 | (2ème nombre) | ||||
|
||||||||||
sn | .......... | s2 | s1 | s0 | (résultat) |
-
le bit du résultat sk est égal à ak ⊕ bk ⊕ ck (addition sans retenue)
-
le bit de retenue ck+1 est égal à (ak ∧ bk) ∨ (ak ∧ ck) ∨ (bk ∧ ck)
Un des exercice de la première feuille de TD consistait à calculer les formes normales conjonctives des formules s ⇔ (a ⊕ b ⊕ c) et cc ⇔ (a ∧ b) ∨ (a ∧ c) ∨ (b ∧ c). On avait trouvé respectivement
(¬a ∨ ¬b ∨ ¬c ∨ s) | ∧ | (¬a ∨ ¬b ∨ c ∨ ¬s) | ∧ | (¬a ∨ b ∨ ¬c ∨ ¬s) | ∧ | (a ∨ ¬b ∨ ¬c ∨ ¬s) | ∧ |
(a ∨ b ∨ c ∨ ¬s) | ∧ | (a ∨ b ∨ ¬c ∨ s) | ∧ | (a ∨ ¬b ∨ c ∨ s) | ∧ | (¬a ∨ b ∨ c ∨ s) |
et
(¬a ∨ ¬b ∨ ¬c ∨ cc) | ∧ | (¬a ∨ ¬b ∨ c ∨ cc) | ∧ | (¬a ∨ b ∨ ¬c ∨ cc) | ∧ | (a ∨ ¬b ∨ ¬c ∨ cc) | ∧ |
(a ∨ b ∨ c ∨ ¬cc) | ∧ | (a ∨ b ∨ ¬c ∨ ¬cc) | ∧ | (a ∨ ¬b ∨ c ∨ ¬cc) | ∧ | (¬a ∨ b ∨ c ∨ ¬cc) |
Créez un fichier xor3-NOM.sat approprié, et en
utilisant votre fonction all_solutions
, vérifiez que les
solutions sont effectivement les valeurs de a, b, c et
s qui vérifient la formule s ⇔ (a ⊕ b ⊕ c).
On a s = (a ⊕ b ⊕ c) ssi 0 = a ⊕ b ⊕ c ⊕ s, c'est à dire ssi un nombre pair de variables sont à 1. Il suffit donc de vérifier à la main ceci sur les solutions, et de vérifier qu'on a bien toutes les possibilités.
Téléchargez le fichier addition-NOM.py.
Écrivez une fonction xor3(s, a, b, c)
qui affiche les clauses
correspondant à l'opération (sur des bits) s := a ⊕ b
⊕ c.
Inspirez-vous pour cela de la fonction carry3(cc, a, b, c)
qui
affiche les clauses correspondant à l'opération cc :=
(a ∧ b) ∨ (a ∧ c) ∨ (b ∧ c).
-
Complétez le fichier addition-NOM.py pour générer un problème SAT correspondant au circuit addition de résultat s :
-
générer toutes les clauses (i=0, ..., n) pour sk := ak ⊕ bk ⊕ ck et ck+1 := (ak ∧ bk) ∨ (ak ∧ ck) ∨ (bk ∧ ck)
-
imposer les valeurs de s0, s1, ... pour obtenir le résultat s.
-
s0 correspond à la parité de s,
-
s1 correspond à la parité de s // 2,
-
s2 correspond à la parité de s // 4,
-
s3 correspond à la parité de s // 8,
-
etc.
-
-
Vérifiez que vous obtenez une solutions correcte pour l'addition a + b == 7, sur 4 bits.
Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.
Pour ceci, vous pouvez lancer le programme avec 2 arguments : la valeur de n (nombre de bits) et la valeurs de s (somme voulue).
$ ./addition-NOM.py 4 7 > add-7.sat
permettra de générer les clauses correspondantes dans le fichier add-7.sat.
Si on ne donne pas de nom de fichier pour la formule, naivesat-NOM.py la lit sur l'entrée standard. Il est donc possible d'utiliser les redirections des shells Unix pour éviter d'avoir à sauvegarder un fichier à chaque fois
$ ./addition-NOM.py 4 7 | ./naivesat-NOM.py -v
>>> the formula contains 17 clauses and 70 variables, for a total of 262 literals
SATISFIABLE
~r1 ~r5 s1 s2 s3 ~s4 a1 ~b1 ~r2 a2 ~b2 ~r3 a3 ~b3 ~r4 ~a4 ~b4
Testez (sur des petites valeurs) en utilisant la fonction
all_solutions
pour vérifiez que vous trouvez bien le bon nombre
de solutions pour a + b == s. (Il y a exactement s+1 manières
d'obtenir s comme la somme de 2 nombres : 0+s, 1+(s-1),
2+(s-2), ..., s + 0.)
Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.
Inverser la multiplication
Inverser un circuit additionneur n'est pas très intéressant. Par contre, inverser un circuit multiplicateur l'est plus. Vérifier la satisfiabilité correspondant à a × b = p (pour un p donné) revient à chercher des facteurs de p, càd à factoriser p !
Générer les clauses correspondant à une multiplication est un peu plus complexe que pour l'addition. Le fichier multiplication-NOM.py le fera pour vous.
-
Ajoutez, tout à la fin du fichier, les clauses pertinentes pour forcer les valeurs de p0, p1, ... p_n aux bits de p. (C'est identique à ce que vous avez fait pour forcer )
Vérifiez que vous arrivez bien à factoriser le nombre 48 en 2 nombres de 4 bits :
$ ./ multiplication.py 4 4 48 | ./naivesat-NOM.py -r SATISFIABLE x_17 ~x_13 ~x_15 ~a_2 ~q_2_2 p_5 ~p_6 ~q_2_1 ~q_1_2 ~x_8 ~q_1_0 ~x_1 ~x_9 ~q_3_0 ~x_6 ~q_2_0 ~q_2_3 b_1 ~q_1_1 ~a_1 x_12 ~q_0_2 a_3 ~x_10 ~x_4 ~b_0 ~q_0_0 ~x_16 ~x_18 ~x_20 ~a_0 p_4 ~q_1_3 ~x_19 ~q_3_3 ~x_2 ~x_7 ~q_0_1 ~x_11 q_3_2 ~p_2 ~x_5 ~q_0_3 ~p_3 ~x_14 q_3_1 ~x_3 ~p_0 ~p_7 ~p_1 ~b_3 b_2
Attention, il y a plusieurs solutions (la factorisation n'est pas unique) et vous n'obtiendrez pas forcément le même résultat. Vous devrez donc extraire les variables a_i et b_j de la solution pour retrouver les valeurs de a et b
-
Vérifiez que pour les clauses correspondant à un nombre premier ne sont pas satisfiable.
Documentez (dans le fichier README) et corrigez les bugs éventuels que vous trouvez.
-
À partir que quelles valeurs de n1 et n2 est-ce que la naivesat-NOM.py devient trop lent (résultat en 30 secondes environ) ?
À combien de variables / clauses est-ce que cela correspond ?
-
Testez les mêmes fichiers avec le programme minisat (paquet Ubuntu / Debian minisat), un SAT solveur très optimisé écrit en C++. À partir de quelles valeurs de n1 et n2 est-ce que minisat devient trop lent ?
minisat lit la formule dans un fichier au format DIMACS. naivesat-NOM.py peut sauvegarder une formule au format DIMACS avec l'option -O / --output:
$ ./multiplication.py 10 10 179 | ./naivesat-NOM.py -O "" formule.cnf $ minisat formule.cnf
Pour les experts du shell, vous pouvez même utiliser les "substitutions de processus" pour éviter d'avoir passer par un fichier :
$ minisat <(./multiplication.py 10 10 179 | ./naivesat-NOM.py -O)
Pour obtenir la solution trouvée, il faut fournir un second nom de fichier à minisat pour qu'il y écrive le résultat :
$ ./multiplication.py 10 10 179 | ./naivesat-NOM.py -O "" formule.cnf $ minisat formule.cnf formule.sol
Pour éviter de passer par un fichier, vous pouvez faire une autre substitution de processus :
$ minisat <(./multiplication.py 10 10 179 | ./naivesat-NOM.py -O) >(cat)
2. Optimisation du solveur naïf : algorithme DPLL
La première avancée sur les solveur a été l'algorithme DPLL. L'idée principale est d'ajouter une phase de simplification avant chaque choix du solveur. Cette étape est parfois appelée BCP, pour Boolean Constraint Propagation.
L'algorithme DPLL proposait 2 simplifications :
-
propagation des clauses unitaires,
-
élimination des littéraux purs.
En pratique, seule la première simplification est importante. La seconde demande en général trop de calculs.
La propagation des clauses unitaires consiste à identifier les clauses avec un unique littéral dans la formule :
Pour satisfaire cette formule, le littéral L doit forcément être à
vrai. On peut donc l'ajouter à la solution courante et simplifier la formule
avec la fonction simplify_formula
écrite précédemment.
Bien entendu, il faut faire cette simplification autant de fois que nécessaire.
-
Ajoutez les quelques lignes suivantes au tout début de la fonction
solve
, juste après la docstring :# boolean constraints propagation if bcp: F, sol2 = BCP(F) sol.extend(sol2)
Ceci permettra d'appeler automatique la fonction de simplification à chaque étape.
-
Complétez le corps de la fonction
BCP(F)
. Cette fonction doit renvoyer 2 choses :-
la formule simplifié au maximum,
-
la liste des littéraux qui ont servi à cette simplification.
Pour information, le corps de ma fonction fait 12 lignes, en comptant les 2 lignes "
sol = []
" et "return F, sol
". -
-
Pour tester, vous pouvez utiliser l'option -D / --DPLL de naivesat-NOM.py.
Décrivez les tests que vous effectuez et comparez l'efficacité du solveur optimisé avec le solveur non optimisé.
-
Comparez l'efficacité du solveur naif optimisé avec minisat et documentez vos résultats.
Testez d'autres optimisations comme :
-
élimination des littéraux purs
-
utilisation du type set de Python pour représenter les clauses : une formule sera représentée par une liste d'ensembles
(Attention, il faudra modifier certaines fonctions du fichier naivesat-NOM.py. Je vous conseille de travailler sur un nouveau fichier naivesat-ensembles.py afin de ne pas casser ce que vous avez fait avant !)