Planification avec PSC (problème de satisfaction de contraintes)
================================================================

L'objectif de cette série d'exercices consistera à planifier la traversée d'un groupe de cannibales et de missionnaires de la rive gauche à la rive droite d'une rivière. Ceux-ci disposent pour ce faire d'un bateau à deux places, qui ne peut être piloté que par un missionnaire. Nous supposons qu'il y a en tout deux missionnaires :math:`M_{1}` et :math:`M_{2}` et deux cannibales :math:`C_{1}` et :math:`C_{2}`. La traversée ne peut se faire qu'en empruntant un unique bateau :math:`B`.
 
Cette série se compose de deux parties. Dans la première partie, correspondant à l'exercice 1, vous devrez concevoir sur papier un modèle PSC qui représente le problème de planification donné au paragraphe précédent et qui permette de le résoudre au moyen d'algorithmes de résolution de PSC. Dans la seconde partie, vous implémenterez ce modèle en Python, et vous le résoudrez en appliquant le module PSC implémenté au cours des exercices précédents.
 
Notez qu'il n'est pas nécessaire de lire l'énoncé de la deuxième partie pour accomplir la première. Vous pourriez en fait éprouver une certaine difficulté à comprendre cet énoncé avant d'avoir lu la solution de la première partie, étant donné qu'il y fait référence.
 
Modules squelettes
------------------

Les modules qui suivent constituent le squelette du programme que nous allons développer. ``exemple_missionnaires.py`` sert en particulier à définir le problème et vous permettra de tester votre implémentation lorsque vous aurez terminé.

:download:`planification.zip <../../squelettes/planification.zip>`

.. ``.../moteur_psc_planification/__init__.py`` :

.. .. include::  ../../squelettes/planification/moteur_psc_planification/__init__.py
    :code:

Module ``.../moteur_psc_planification/axiomecadre.py`` :

.. include::  ../../squelettes/planification/moteur_psc_planification/axiomecadre.py
    :code:

.. Module ``.../moteur_planification/__init__.py`` :

.. .. include::  ../../squelettes/planification/moteur_planification/__init__.py
    :code:

Module ``.../moteur_planification/operateur.py`` :

.. include::  ../../squelettes/planification/moteur_planification/operateur.py
    :code:

Module``.../moteur_planification/etat.py`` :

.. include::  ../../squelettes/planification/moteur_planification/etat.py
    :code:

Module ``.../moteur_planification/planification.py`` :

.. include::  ../../squelettes/planification/moteur_planification/planification.py
    :code:

Module ``.../exemple_missionnaires.py`` :

.. include::  ../../squelettes/planification/exemple_missionnaires.py
    :code:

Veillez à respecter la structure des dossiers telle qu'elle est reflétée dans les noms des modules ci-dessus, sous peine de devoir modifier les instructions ``import``.

Exercice 1 : Modélisation sur papier
------------------------------------

Avant de commencer à coder, vous devez modéliser le problème de planification sous forme de PSC. Pour ce faire, il convient de procéder en deux étapes : 

* Définition du problème de planification en termes de propositions et d'opérateurs ;
* Définition d'un PSC correspondant, en termes de variables et de contraintes sur ces variables.

Définition du problème de planification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Un problème de planification peut être défini par trois éléments :

* Un ensemble de propositions qui décrivent complètement l'état du monde à un moment donné. Certaines de ces propositions peuvent être mutuellement exclusives. Il faut alors expliciter les contraintes d'exclusion.
* Deux ensembles d'instanciations partielles de ces propositions, qui décrivent respectivement l'état initial et l'état final, qui est le but à atteindre.
*  Un ensemble d'opérateurs qui permettent de faire évoluer le monde d'un état à un autre. 

Rappelons qu'une proposition est, par définition, une affirmation portant sur l'état d'une partie du monde et qui peut être vraie ou fausse. Les opérateurs, quant à eux, se définissent comme des actions dont l'exécution nécessite que certaines propositions (leurs préconditions) soient vraies ou fausses, et qui ont pour conséquence d'imposer à certaines propositions (leurs postconditions) d'être vraies ou fausses. 

Pour commencer, proposez donc une définition d'un problème de planification qui corresponde à la description informelle du problème telle qu'elle vous est donnée en introduction. 

Privilégiez un modèle simple, qui ne contienne pas trop d'opérateurs superflus. Par exemple, il est inutile d'introduire des opérateurs pour décrire le fait qu'un missionnaire ou un cannibale embarque sur le bateau ou qu'il en débarque. Pour chaque acteur, vous pouvez utiliser une proposition indiquant s'il se trouve ou non sur la rive gauche, et une autre proposition indiquant s'il se trouve ou non sur la rive droite.

Définition d'un PSC correspondant
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Par définition, un PSC est décrit par : 

*  Un ensemble de variables, qui prennent des valeurs dans des domaines définis ;
*  Un ensemble de contraintes sur ces variables, qui définissent les combinaisons de valeurs admissibles.

Proposez un modèle PSC pour le problème de planification. Les variables doivent décrire complètement les propositions et les opérateurs lors de chaque état. Les contraintes, quant à elles, doivent exprimer les propriétés et les limitations du problème. Par exemple, un bateau ne peut contenir que deux acteurs au maximum, et l'un d'eux (le pilote) doit être un missionnaire. Ou encore, le bateau doit initialement être à gauche pour pouvoir faire la traversée de gauche à droite. 

Afin de reformuler le problème de planification sous la forme d'un PSC, vous devrez faire une hypothèse sur le nombre d'états nécessaires pour l'existence d'un plan aboutissant à une solution (c'est-à-dire sur le nombre d'applications successives d'opérateurs). Notez que votre modèle PSC n'est pas obligé de se limiter aux contraintes unaires ou binaires. Il peut comporter des contraintes :math:`n`-aires avec :math:`n > 2`. Par exemple, les contraintes correspondant aux axiomes de cadre.

..  Le module PSC que vous avez implémenté dans les labos précédents ne connaît que les contraintes unaires ou binaires, pas les contraintes n-aires portant sur au moins trois variables.  Il vous suffit d'étendre les capacités du module PSC pour qu'il supporte ces contraintes n-aires. Les exercices suivants détaillent la procédure à suivre. 

Exercice 2 : Construction du problème de planification
------------------------------------------------------

Le module ``exemple_missionnaires.py`` contient une routine qui, pour commencer, définit les acteurs, opérateurs, mutex et conditions de départ et de fin du problème, puis construit le problème de planification en utilisant la classe ``Planification`` du module ``planification.py``. Une fois l'objet ``Planification`` construit, sa méthode ``resoudre`` utilise les outils de résolution de PSC développés les séries précédentes pour résoudre le problème.

Construction des propositions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rédigez le code nécessaire pour contruire les propositions du problème. Une proposition doit être simplement représentée par une string. Stockez toutes les propositions dans la liste ``propositions``. 

Construction des opérateurs
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Construisez à présent les opérateurs du problème, qui seront représentés par la classe ``Operateur`` du module ``operateur.py``. Le constructeur de cette classe prend comme arguments trois paramètres : 

*  Le nom de l'opérateur à créer (similaire à la représentation des propositions) ;
*  La liste des préconditions (une liste de propositions) ;
*  La liste des postconditions (une liste de propositions).

Spécification des mutex de propositions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Ajoutez le code nécessaire à construire les mutex de propositions et qui les stockera dans une liste sous forme de tuples ``(prop1, prop2)``. ``prop1`` et ``prop2`` sont ainsi deux propositions qui ne doivent pas être vraies en même temps. 

Spécification des mutex d'opérateurs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Ajoutez ensuite le code qui définira les mutex d'opérateurs avec le même format que les mutex de propositions, c'est-à-dire comme une liste de tuples ``(op1, op2)``, avec ``op1`` et ``op2`` deux opérateurs qui ne doivent pas être exécutés en même temps.

Déclaration des contraintes initiales et finales
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Spécifiez maintenant les contraintes initiales et finales du problème de planification avec deux listes de tuples ``(proposition, valeur)``.

Exercice 3 : Implémentation des axiomes de cadre, états et planificateur
------------------------------------------------------------------------

Les modules ``axiomecadre.py``, ``etat.py`` et ``planification.py`` contiennent les classes et les algorithmes qui permettent de modéliser un problème de planification comme un PSC, avant de résoudre celui-ci pour trouver un plan valide. 
Le module PSC utilisé sera celui qui a été développé au cours des exercices précédents. 

La classe ``ContrainteAxiomeCadre``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Cette classe, définie dans ``axiomecadre.py``, est une sous-classe de la classe ``Contrainte`` et implémente une contrainte d'axiome de cadre pour un état :math:`S_{i}` donné et une proposition prop donnée :

Si :math:`prop(S_{i}=False)` et :math:`prop(S_{i+1})=True`, alors, pour au moins un opérateur :math:`op` qui a :math:`prop` comme postcondition, on a :math:`(opS_{i})=True`.

Cette contrainte est une contrainte :math:`n`-aire qui porte sur plus de deux variables. Ces variables sont les attributs de la classe :

* ``var_pre`` est la variable :math:`prop(S_{i})` ;
* ``var_post`` est la variable :math:`prop(S_{i+1})` ;
* ``vars_ops`` est la liste des variables correspondant aux opérateurs qui ont prop comme postcondition.

La méthode ``est_valide``
__________________________

Il vous faut tout d'abord implémenter la méthode ``est_valide``. Notez que contrairement au cas des contraintes unaires et binaires, cette méthode peut être appelée alors que toutes les variables de la contrainte ne sont pas encore instanciées (c'est-à-dire même quand leur valeur est ``None``). Traitez donc ce cas en premier, et faites une hypothèse de présomption de validité : la contrainte est présumée valide tant qu'on n'a pas pu prouver qu'elle était violée (c'est-à-dire tant qu'au moins une de ses variables n'est pas encore instanciée).

La méthode ``propage``
______________________

Implémentez ensuite la méthode ``propage``. Cette méthode est appelée juste après qu'une valeur ait été choisie pour une variable de la contrainte. Elle tente alors de propager les conséquences de ce choix aux variables non encore instanciées de la contrainte pour réduire leurs labels. En effet, il est possible que l'assignation d'une valeur à une variable rende incompatibles certaines valeurs des labels des variables non encore instanciées. Cette méthode doit retourner ``True`` si et seulement si aucune inconsistance n'a été découverte.
 
Imaginons par exemple que la seule variable déjà instanciée est :math:`prop(S_{i})=False`, et qu'on désire propager aux variables d'opérateurs les conséquences de l'assignation :math:`prop(S_{i+1})=Flase`. Il est clair que la contrainte sera alors toujours vérifiée et que la méthode retournera ``True`` sans avoir pu découvrir aucune valeur incompatible dans les labels des variables d'opérateurs. Inversement, si l'on choisit l'assignation :math:`prop(S_{i+1})=True`, on peut en déduire qu'au moins un des labels des variables d'opérateurs doit contenir la valeur ``True``. Si ce n'est pas le cas, cette assignation est inconsistante. Si seulement une des variables d'opérateurs possède un label qui contient ``True``, alors on peut d'ores et déjà conclure que seule la valeur ``True`` est possible pour cette variable, et on peut retirer la valeur ``False`` de son label.
 
Comme vous le soupçonnez peut-être déjà sur la base de cet exemple, l'implémentation d'un algorithme de propagation performant pour une contrainte :math:`n`-aire s'avère être un problème difficile dans le cas général, surtout si l'on veut accélérer la recherche en découvrant le plus tôt possible les inconsistances et en réduisant au maximum les labels des variables non encore instanciées.

Dans cet exercice, nous vous proposons d'en implémenter une version simple, peu performante mais suffisante pour le problème de planification qui nous occupe. Cette implémentation paresseuse consiste à ne tenter de réduire les labels et de détecter les inconsistances que lorsqu'il ne reste plus qu'une seule variable de la contrainte qui ne soit pas encore instanciée. Lorsque c'est le cas, vérifiez simplement les valeurs du label de cette variable une par une, et retirez du label celles qui ne respectent pas la contrainte. Retournez ``True`` si et seulement si le label résultant n'est pas vide. Dans le cas contraire, lorsqu'au moins deux variables de la contraintes ne sont pas encore instanciées, utilisez la même hypothèse de présomption de validité que pour la méthode ``est_valide``, et retournez systématiquement ``True`` sans vous mettre en peine de réduire les labels de ces variables. 

.. Comme exercice optionnel, vous pourrez plus tard implémenter un algorithme de propagation plus performant, que vous comparerez avec ce premier algorithme simple sur des problèmes de planification plus complexes.

Remarque sur la méthode ``reviser``
___________________________________

Notez que l'implémentation de la fonction ``reviser`` qui vous est fournie retourne simplement ``False``, c'est-à-dire qu'elle n'essaie pas de réduire les domaines des variables en appliquant la consistance des arcs. La raison en est que la consistance des arcs n'est pas définie pour des contraintes :math:`n`-aires. Pour ces contraintes, on parle plutôt de consistance des arcs généralisée (*Generalized Arc Consistency*, ou GAC) : pour chaque valeur du domaine de chaque variable, il doit exister une combinaison de valeurs pour toutes les autres variables qui satisfasse la contrainte. Mais dans l'exemple simple qui nous occupe, il n'est pas nécessaire d'implémenter la GAC, de même qu'il n'est pas nécessaire d'implémenter une méthode de propagation très performante.
 
La classe ``Etat``
~~~~~~~~~~~~~~~~~~

Un état contient six attributs :

*  ``vars_initiales`` : une liste de variables correspondant aux propositions au début de l'état (égales à celles qui existent à la fin de l'état précédent). Cette ``liste'' est en fait un dictionnaire qui associe les propositions à leurs variables respectives. 
*  ``vars_finales`` : un dictionnaire de variables associées aux propositions à la fin de l'état (égales à celles du début de l'état suivant).
*  ``vars_operateurs`` : un dictionnaire de variables associées aux opérateurs pour cet état.
*  ``no_etat`` : le numéro de l’état, inclus dans l’intervalle [0, ``Planification.nb_etats``).
*  ``etat_prec``: l'objet ``Etat`` qui précède l'état courant dans le plan.

Le constructeur vous est donné, et appelle les méthodes ``construire_vars_operateurs`` et ``construire_vars_propositions``, qui remplissent les attributs ``vars_operateurs``, et ``vars_initiales`` et ``vars_finales`` respectivement.  

La première de ces méthodes est déjà implémentée. Vous devez coder la seconde en vous inspirant de la première. Nommez les variables à l'aide du numéro de l'état au début duquel se trouve la variable. N'oubliez pas que les variables finales d'un état doivent être les mêmes que les variables initiales de l'état suivant.

La classe ``Planification``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

La classe ``Planification`` est la classe centrale du planificateur. Elle transforme un problème de planification en un PSC afin de découvrir un plan valide. Cette classe possède les attributs suivants :

* ``propositions``, ``operateurs``, ``mutex_propositions``, ``mutex_operateurs``, ``depart`` et ``but``, qui correspondent aux listes construites dans ``exemple_missionnaires.py`` ;
* ``nb_etats`` : le nombre d'états dans le plan, c'est-à-dire la longueur de celui-ci ;
* ``etats``: la liste des états du problème ;
* ``psc`` : l'instance de PSC qui représente le problème modélisé en PSC.

Les méthodes de la classe
_________________________

Vous allez maintenant implémenter les méthodes de la classe ``Planification``. Ce sont les suivantes :

*  ``contruire_etats`` : construit tous les états de la planification et les ajoute à la liste ``self.etats``. Faites en sorte que la liste soit triée par ordre croissant du numéro de l'état. 
*  ``constuire_contraintes_propositions`` : construit les contraintes binaires d'exclusion mutuelle entre propositions.
*  ``construire_contraintes_operateurs`` : construit les contraintes binaires d'exclusion mutuelle entre opérateurs.
*  ``construire_contraintes_initiales`` et ``construire_contraintes_finales``: ajoutent les contraintes initiales sur les propositions de l’état 0 et les contraintes finales sur les propositions de l'état final. 
*  ``constuire_contraintes_conditions``: ajoute les contraintes de pré- et postconditions entre propositions et opérateurs. 
*  ``construire_contraintes_axiomes_cadre``: ajoute les contraintes d'axiomes de cadre en utilisant la classe ``ContrainteAxiomeCadre``.

Test du programme
-----------------

Finalement, testez votre programme en lançant ``exemple_missionaires.py``.
