Consignes
La collaboration orale est encouragée, mais le partage de code et le copier/coller (via Discord, github, etc.) n'est pas autorisé et pourra être pénalisé.
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 simplesat-NOM.py
-
vos fichier question2-NOM.sat, cartes-NOM.sat et zebra-NOM.py
-
vos fichiers addition-NOM.py et multiplication-NOM.py
-
un fichier texte README contenant les réponses aux questions du TP
-
tout autre fichier que vous jugerez pertinent (fichiers de tests, graphiques, benchmarks, etc.)
Vous aurez compris que NOM devra être remplacé par votre nom (ou vos noms en cas de binômes) !
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 simplesat-NOM.py (Python), à compléter
-
les exemples mentionnés dans le sujet test0.sat, test1.sat, test2.sat, test3.sat
-
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 : simplesat.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.
Les fichiers .sat 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 d'extension .sat.
-
Les lignes qui commencent par "#" sont ignorées, ce sont des commentaires.
-
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.
Les noms de variables ne peuvent contenir que les caractères suivants :
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_@ mais ne commencez pas un nom de variable par le symbole "_".
-
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 simplesat
Le fichier simplesat-NOM.py contient un solveur SAT récursif naïf écrit en Python, utilisable uniquement en ligne de commandes.
Vous devrez le compléter (question 1) pour le rendre fonctionnel, mais voici comment l'utiliser pour chercher une solution à une formule :
$ python3 ./simplesat-NOM.py FICHIER_FORMULE
où FICHIER_FORMULE est le fichier (au format SAT) qui contient la formule à considérer
Par exemple :
$ python3 ./simplesat-NOM.py test2.sat
SATISFIABLE
-a b c d
$ python3 ./simplesat-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 simplesat-NOM.py sont :
-
Si vous donnez l'option -T ou --test à simplesate.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 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).
CE N'EST PAS PRATIQUE !
Représentation des formule FNC et code du solveur
En interne, les formules traitées par simplesat-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]]
Attention, l'entier associé à une variable est local au code Python.
Suivant la formule utilisée, la variable x3 peut être
représentée par un entier différent de 3
. (Le littéral ¬
x3 sera bien entendu représentée par l'entier opposé à celui
représentant x3.)
La première fonction définie dans simplesat-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 :$ python3 ./simplesat-NOM.py test0.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 !
$ python3 ./simplesat-NOM.py FICHIER_FORMULE --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]]
(Attention, il faut remplacer FICHIER_FORMULE par un nom de fichier !)
-
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 sans utiliser l'option -A / --all ? (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. Pour ceci, vous pouvez donner l'argument -A ou --all à simplesat-NOM.py.
Testez plusieurs fois simplesat-NOM.py avec l'option -A sur la formule précédente) en mélangeant aléatoirement la formule avec l'option -r / --randomize. (Cette option modifie simplement l'ordre des clauses de la formule et celui des littéraux des clauses, mais ne change pas la sémantique de la formule.)
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 et le petit carré k contient au plus un 1, etc.
-
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, dont une moitié sert à lire / écrire la grille.)
1.1. L'énigme de Einstein
Comme simplesat-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.
L'objectif de la question suivante sera de répondre à 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 cœur est en position 3 -
...
-
-
le premier indice donne 2 clauses ne contenant qu'un seul littéral chacune
-
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 simple.)
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 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 les suivantes. Elles expriment le fait qu'on ne peut pas trouver 2 as de piques sur les positions 1, 2, 3 et 4.
# 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 simplesat-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.
L'option -X / --exclude-negative-lit de simplesat-NOM.py permet de n'afficher que les variables vraies des solutions. Cela facilite l'interprétation des solution pour cette question (ou la suivante) : si l'as de pique est en position 3, les littéraux -X_1_pique, -X_2_pique et -X_4_pique n'apparaitront pas dans la solution affichée.
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.
Et les indices sont les suivants :
-
le musicien a une tortue ;
-
le propriétaire de la maison jaune écoute du jazz ;
-
la personne qui écoute du hardrock 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.
L'objectif de la question suivante est de répondre à 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 avec 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é) et 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 (10 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
same_position(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
all_attr_pos()
-
"aucun attribut n'est à 2 emplacements différents" générée par la fonction
no_duplicates()
-
"il n'y a pas 2 attributs d'une même catégorie au même emplacement" générée par la fonction
single_attr_pos()
Les fonctions no_duplicates
et single_attr_pos
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
same_position
(voir ci dessus), -
écrivez le corps de la fonction
all_attr_pos
(voir ci dessus), -
complétez avec les appels pertinents pour chacun des indices.
L'exécution de ce programme
$ python3 zebra-NOM.py ...
affiche la formule CNF correspondant au problème de Einstein.
-
-
Lancez simplesat-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 une redirection du shell :
$ python3 zebra-NOM.py > formule-zebre.sat
Comme simplesat-NOM.py peut lire la formule sur l'entrée standard, vous pouvez aussi utiliser une redirection
$ python3 zebra-NOM.py | python3 simplesat-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
all_attr_pos
, no_duplicates
et
single_attr_pos
) avant les indices pour constater une grosse
différence de temps de calcul !
De la même manière, remplacer "tous les attributs sont à un
emplacement" ("constraint_1
) par la contrainte "tous les
emplacements ont un attribut"
(E_1_chanson ∨ E_1_classique ∨ E_1_hardrock ∨ E_1_hiphop ∨ E_1_jazz) | ∧ |
(E_1_biere ∨ E_1_eau ∨ E_1_soda ∨ E_1_the ∨ E_1_whisky) | ∧ |
............. | ∧ |
(E_2_chanson ∨ E_2_classique ∨ E_2_hardrock ∨ E_2_hiphop ∨ E_2_jazz) | ∧ |
............. etc. ............. |
donne une formule équivalente (grâce à no_duplicates
et
single_attr_pos
), mais où la satisfiabilité est beaucoup plus
dure pour l'algorithme naïf.
Normalement, il n'y a qu'une seule solution au problème ! Si vous lancez simplesat-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.
2. Algorithme DPLL
La première avancée sur les solveurs 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, on peut faire cette simplification jusqu'à ce que la formule ne contienne plus de clause unitaire.
-
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ée 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 simplesat-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 naïf optimisé avec minisat et documentez vos résultats.
Vous pouvez utiliser la formule de l'énigme de Einstein, le fichier modélisant une grille de sudoku. Pour des fichiers plus gros, voici par exemple une archive de fichiers modélisant le problème des n reines, pour plusieurs valeurs de n. Les fichiers sont donnés au format SAT (extension .sat) et DIMACS (extension .cnf).
minisat lit la formule dans un fichier au format DIMACS. simplesat-NOM.py peut transformer un fichier .sat au format DIMACS avec l'option -C / --convert. Vous pouvez donc sauvegarder le fichier DIMACS avec une redirection :
$ python3 simplesat-NOM.py -C formule.sat > formule.cnf
$ minisat formule.cnf
Pour obtenir la solution trouvée, il faut fournir un second nom de fichier à minisat pour qu'il y écrive le résultat :
$ minisat formule.cnf formule.sol
$ cat formule.sol
...
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, pour cette optimisation, il faudra modifier certaines fonctions du fichier simplesat-NOM.py. Je vous conseille de travailler sur un nouveau fichier simplesat-ensembles.py afin de ne pas casser ce que vous avez fait avant !)
Pour optimiser encore plus, il faudrait utiliser un langage compilé (C ou C++) et remplacer la version récursive par une version itérative utilisant des structures de données spécialisées. Vous pouvez aller voir le TP2 de l'année 2020 pour découvrir ce à quoi ça pourrait ressembler !
Pour optimiser encore plus, il faudrait par exemple implémenter le conflict-driven clause learning.
3. Autres problèmes de satisfiabilité : 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 correspondant à des opérations arithmétique.
3.1. 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, "carry" en anglais) | |||||
an | .......... | a2 | a1 | a0 | (1er nombre) | |||||
+ | bn | .......... | b2 | b1 | b0 | (2ème nombre) | ||||
|
||||||||||
sn | .......... | s2 | s1 | s0 | (somme) |
-
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) (au moins 2 "1" parmi ak, bk et ck)
Les formes normales conjonctives des formules s ⇔ (a ⊕ b ⊕ c) et cc ⇔ (a ∧ b) ∨ (a ∧ c) ∨ (b ∧ c) sont 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) |
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) en utilisant
xor3
etcarry3
.Attention, pour la fonction
carry3
, le premier argument correspond à la retenue de la colonne suivante. Vous devrez donc l'appeler avec, par exemple, les arguments"c_3"
,"a_2"
,"b_2
" et"c_2"
. -
imposer les valeurs de
s_0
,s_1
, ... pour obtenir le résultats
.
-
s_0
correspond à la parité des
, -
s_1
correspond à la parité des // 2
, -
s_2
correspond à la parité des // 4
, -
s_3
correspond à la parité des // 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 devez lancer le programme avec 2 arguments : la valeur de
n
(nombre de bits) et la valeurs des
(somme voulue).$ python3 addition-NOM.py 4 7 > add-7.sat
permettra de générer les clauses correspondantes dans le fichier add-7.sat.
Il est possible d'utiliser les redirections des shells Unix pour éviter d'avoir à sauvegarder un fichier à chaque fois :
$ python3 addition-NOM.py 4 7 | python3 simplesat-NOM.py -v >>> the formula contains 17 clauses and 70 variables, for a total of 262 literals SATISFIABLE -c_1 -c_5 s_1 s_2 s_3 -s_4 a_1 -b_1 -c_2 a_2 -b_2 -c_3 a_3 -b_3 -c_4 -a_4 -b_4
-
Testez (sur des petites valeurs) en utilisant l'option -A / --all 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.
3.2. 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
p_0
,p_1
, ...p_{n}
aux bits dep
. (C'est identique à ce que vous avez fait pour forcer les valeurs des_0
, ... dans la question précédente.)Vérifiez que vous arrivez bien à factoriser le nombre 48 en 2 nombres de 4 bits :
$ python3 multiplication-NOM.py 4 4 48 | python3 simplesat-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.
-
utilisez simplesat avec l'option DPLL pour vérifier que des nombres sont premiers.
-
À partir que quelles valeurs de n1, n2 et p est-ce que cela devient trop lent (résultat en 30 secondes environ) ? (Vous pouvez trouvez la liste de tous les nombres premiers ici: allprimes (L. Silvestre))
À combien de variables / clauses est-ce que cela correspond ? (Vous pouvez utiliser l'option -v / --verbose de simplesat-NOM.py pour afficher cette information.)
À votre avis, est-ce une bonne méthode pour tester si un nombre est premier ?
-
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. simplesat-NOM.py peut transformer un fichier .sat au format DIMACS avec l'option -C / --convert:
$ python3 multiplication-NOM.py 10 10 179 | python3 simplesat-NOM.py -C - formule.cnf $ minisat formule.cnf
Pour obtenir la solution trouvée, il faut fournir un second nom de fichier à minisat pour qu'il y écrive le résultat :
$ python3 multiplication-NOM.py 10 10 179 | python3 simplesat-NOM.py -C - formule.cnf $ minisat formule.cnf formule.sol $ cat formule.sol ...
Pour les experts du shell, vous pouvez même utiliser les "substitutions de processus" pour éviter d'avoir passer par des fichier :
$ minisat <(python3 multiplication-NOM.py 10 10 179 | python3 simplesat-NOM.py -C) >(cat)