Salome HOME
Documentation and source correction and improvements for DFO
[modules/adao.git] / src / daComposant / daCore / AssimilationStudy.py
index f7b1cd27a42e90e2667ece0e70f0882d6d18b805..fe3233ef92b80eaa0c75193829151325f6c6d58c 100644 (file)
@@ -1,36 +1,47 @@
 #-*-coding:iso-8859-1-*-
 #
-#  Copyright (C) 2008-2010  EDF R&D
+# Copyright (C) 2008-2015 EDF R&D
 #
-#  This library is free software; you can redistribute it and/or
-#  modify it under the terms of the GNU Lesser General Public
-#  License as published by the Free Software Foundation; either
-#  version 2.1 of the License.
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License.
 #
-#  This library is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-#  Lesser General Public License for more details.
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
 #
-#  You should have received a copy of the GNU Lesser General Public
-#  License along with this library; if not, write to the Free Software
-#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 #
-#  See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
 #
-__doc__ = """
-    Définit les outils généraux élémentaires.
-    
-    Ce module est destiné à etre appelée par AssimilationStudy pour constituer
-    les objets élémentaires de l'algorithme.
+# Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
+
+"""
+    Classe principale pour la préparation, la réalisation et la restitution de
+    calculs d'assimilation de données.
+
+    Ce module est destiné à être appelé par AssimilationStudy pour constituer
+    les objets élémentaires de l'étude.
 """
-__author__ = "Jean-Philippe ARGAUD - Mars 2008"
+__author__ = "Jean-Philippe ARGAUD"
+__all__ = ["AssimilationStudy"]
 
 import os, sys
 import numpy
-import Logging ; Logging.Logging() # A importer en premier
+import ExtendedLogging ; ExtendedLogging.ExtendedLogging() # A importer en premier
+import logging
+try:
+    import scipy.optimize
+    logging.debug("Succeed initial import of scipy.optimize with Scipy %s", scipy.version.version)
+except ImportError:
+    logging.debug("Fail initial import of scipy.optimize")
 import Persistence
-from BasicObjects import Operator
+from BasicObjects import Operator, Covariance
+from PlatformInfo import uniq
 
 # ==============================================================================
 class AssimilationStudy:
@@ -45,53 +56,55 @@ class AssimilationStudy:
         élémentaire. Ces variables sont ensuite disponibles pour implémenter un
         algorithme élémentaire particulier.
 
-        Background............: vecteur Xb
-        Observation...........: vecteur Y (potentiellement temporel)
-            d'observations
-        State.................: vecteur d'état dont une partie est le vecteur de
-            contrôle. Cette information n'est utile que si l'on veut faire des
-            calculs sur l'état complet, mais elle n'est pas indispensable pour
-            l'assimilation.
-        Control...............: vecteur X contenant toutes les variables de
-            contrôle, i.e. les paramètres ou l'état dont on veut estimer la
-            valeur pour obtenir les observations
-        ObservationOperator...: opérateur d'observation H
+        - Background : vecteur Xb
+        - Observation : vecteur Y (potentiellement temporel) d'observations
+        - State : vecteur d'état dont une partie est le vecteur de contrôle.
+          Cette information n'est utile que si l'on veut faire des calculs sur
+          l'état complet, mais elle n'est pas indispensable pour l'assimilation.
+        - Control : vecteur X contenant toutes les variables de contrôle, i.e.
+          les paramètres ou l'état dont on veut estimer la valeur pour obtenir
+          les observations
+        - ObservationOperator...: opérateur d'observation H
 
         Les observations présentent une erreur dont la matrice de covariance est
         R. L'ébauche du vecteur de contrôle présente une erreur dont la matrice
         de covariance est B.
         """
         self.__name = str(name)
-        self.__Xb = None
-        self.__Y  = None
-        self.__B  = None
-        self.__R  = None
-        self.__Q  = None
-        self.__H  = {}
-        self.__M  = {}
+        self.__Xb  = None
+        self.__Y   = None
+        self.__U   = None
+        self.__B   = None
+        self.__R   = None
+        self.__Q   = None
+        self.__HO  = {}
+        self.__EM  = {}
+        self.__CM  = {}
         #
         self.__X  = Persistence.OneVector()
-        self.__Parameters = {}
+        self.__Parameters        = {}
         self.__StoredDiagnostics = {}
+        self.__StoredInputs      = {}
         #
         # Variables temporaires
-        self.__algorithm     = {}
-        self.__algorithmFile = None
-        self.__algorithmName = None
-        self.__diagnosticFile = None
+        self.__algorithm         = {}
+        self.__algorithmFile     = None
+        self.__algorithmName     = None
+        self.__diagnosticFile    = None
         #
         # Récupère le chemin du répertoire parent et l'ajoute au path
         # (Cela complète l'action de la classe PathManagement dans PlatformInfo,
         # qui est activée dans Persistence)
         self.__parent = os.path.abspath(os.path.join(os.path.dirname(__file__),".."))
         sys.path.insert(0, self.__parent)
-        sys.path = list(set(sys.path)) # Conserve en unique exemplaire chaque chemin
+        sys.path = uniq( sys.path ) # Conserve en unique exemplaire chaque chemin
 
     # ---------------------------------------------------------
     def setBackground(self,
             asVector           = None,
             asPersistentVector = None,
             Scheduler          = None,
+            toBeStored         = False,
             ):
         """
         Permet de définir l'estimation a priori :
@@ -100,25 +113,57 @@ class AssimilationStudy:
         - asPersistentVector : entrée des données, comme un vecteur de type
           persistent contruit avec la classe ad-hoc "Persistence"
         - Scheduler est le contrôle temporel des données
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
         if asVector is not None:
-            if type( asVector ) is type( numpy.matrix([]) ):
-                self.__Xb = numpy.matrix( asVector.A1, numpy.float ).T
-            else:
-                self.__Xb = numpy.matrix( asVector,    numpy.float ).T
+            self.__Xb = numpy.matrix( numpy.ravel(numpy.matrix(asVector)), numpy.float ).T
         elif asPersistentVector is not None:
-            self.__Xb = asPersistentVector
+            if type(asPersistentVector) in [type([]),type(()),type(numpy.array([])),type(numpy.matrix([]))]:
+                self.__Xb = Persistence.OneVector("Background", basetype=numpy.matrix)
+                for member in asPersistentVector:
+                    self.__Xb.store( numpy.matrix( numpy.asmatrix(member).A1, numpy.float ).T )
+            else:
+                self.__Xb = asPersistentVector
         else:
-            raise ValueError("Error: improperly defined background")
+            raise ValueError("Error: improperly defined background, it requires at minima either a vector, a list/tuple of vectors or a persistent object")
+        if toBeStored:
+            self.__StoredInputs["Background"] = self.__Xb
         return 0
-    
-    def setBackgroundError(self, asCovariance=None):
+
+    def setBackgroundError(self,
+            asCovariance  = None,
+            asEyeByScalar = None,
+            asEyeByVector = None,
+            asCovObject   = None,
+            toBeStored    = False,
+            toBeChecked   = False,
+            ):
         """
         Permet de définir la covariance des erreurs d'ébauche :
         - asCovariance : entrée des données, comme une matrice compatible avec
           le constructeur de numpy.matrix
+        - asEyeByScalar : entrée des données comme un seul scalaire de variance,
+          multiplicatif d'une matrice de corrélation identité, aucune matrice
+          n'étant donc explicitement à donner
+        - asEyeByVector : entrée des données comme un seul vecteur de variance,
+          à mettre sur la diagonale d'une matrice de corrélation, aucune matrice
+          n'étant donc explicitement à donner
+        - asCovObject : entrée des données comme un objet ayant des méthodes
+          particulieres de type matriciel
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
-        self.__B  = numpy.matrix( asCovariance, numpy.float )
+        self.__B = Covariance(
+            name          = "BackgroundError",
+            asCovariance  = asCovariance,
+            asEyeByScalar = asEyeByScalar,
+            asEyeByVector = asEyeByVector,
+            asCovObject   = asCovObject,
+            toBeChecked   = toBeChecked,
+            )
+        if toBeStored:
+            self.__StoredInputs["BackgroundError"] = self.__B
         return 0
 
     # -----------------------------------------------------------
@@ -126,6 +171,7 @@ class AssimilationStudy:
             asVector           = None,
             asPersistentVector = None,
             Scheduler          = None,
+            toBeStored         = False,
             ):
         """
         Permet de définir les observations :
@@ -134,31 +180,65 @@ class AssimilationStudy:
         - asPersistentVector : entrée des données, comme un vecteur de type
           persistent contruit avec la classe ad-hoc "Persistence"
         - Scheduler est le contrôle temporel des données disponibles
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
         if asVector is not None:
-            if type( asVector ) is type( numpy.matrix([]) ):
-                self.__Y = numpy.matrix( asVector.A1, numpy.float ).T
-            else:
-                self.__Y = numpy.matrix( asVector,    numpy.float ).T
+            self.__Y = numpy.matrix( numpy.ravel(numpy.matrix(asVector)), numpy.float ).T
         elif asPersistentVector is not None:
-            self.__Y = asPersistentVector
+            if type(asPersistentVector) in [type([]),type(()),type(numpy.array([])),type(numpy.matrix([]))]:
+                self.__Y = Persistence.OneVector("Observation", basetype=numpy.matrix)
+                for member in asPersistentVector:
+                    self.__Y.store( numpy.matrix( numpy.asmatrix(member).A1, numpy.float ).T )
+            else:
+                self.__Y = asPersistentVector
         else:
-            raise ValueError("Error: improperly defined observations")
+            raise ValueError("Error: improperly defined observations, it requires at minima either a vector, a list/tuple of vectors or a persistent object")
+        if toBeStored:
+            self.__StoredInputs["Observation"] = self.__Y
         return 0
 
-    def setObservationError(self, asCovariance=None):
+    def setObservationError(self,
+            asCovariance  = None,
+            asEyeByScalar = None,
+            asEyeByVector = None,
+            asCovObject   = None,
+            toBeStored    = False,
+            toBeChecked   = False,
+            ):
         """
         Permet de définir la covariance des erreurs d'observations :
         - asCovariance : entrée des données, comme une matrice compatible avec
           le constructeur de numpy.matrix
+        - asEyeByScalar : entrée des données comme un seul scalaire de variance,
+          multiplicatif d'une matrice de corrélation identité, aucune matrice
+          n'étant donc explicitement à donner
+        - asEyeByVector : entrée des données comme un seul vecteur de variance,
+          à mettre sur la diagonale d'une matrice de corrélation, aucune matrice
+          n'étant donc explicitement à donner
+        - asCovObject : entrée des données comme un objet ayant des méthodes
+          particulieres de type matriciel
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
-        self.__R  = numpy.matrix( asCovariance, numpy.float )
+        self.__R = Covariance(
+            name          = "ObservationError",
+            asCovariance  = asCovariance,
+            asEyeByScalar = asEyeByScalar,
+            asEyeByVector = asEyeByVector,
+            asCovObject   = asCovObject,
+            toBeChecked   = toBeChecked,
+            )
+        if toBeStored:
+            self.__StoredInputs["ObservationError"] = self.__R
         return 0
 
     def setObservationOperator(self,
-            asFunction = {"Direct":None, "Tangent":None, "Adjoint":None},
+            asFunction = None,
             asMatrix   = None,
             appliedToX = None,
+            toBeStored = False,
+            avoidRC    = True,
             ):
         """
         Permet de définir un opérateur d'observation H. L'ordre de priorité des
@@ -166,6 +246,11 @@ class AssimilationStudy:
         - si asFunction["Tangent"] et asFunction["Adjoint"] ne sont pas None
           alors on définit l'opérateur à l'aide de fonctions. Si la fonction
           "Direct" n'est pas définie, on prend la fonction "Tangent".
+          Si "useApproximatedDerivatives" est vrai, on utilise une approximation
+          des opérateurs tangents et adjoints. On utilise par défaut des
+          différences finies non centrées ou centrées (si "withCenteredDF" est
+          vrai) avec un incrément multiplicatif "withIncrement" de 1% autour
+          du point courant ou sur le point fixe "withdX".
         - si les fonctions ne sont pas disponibles et si asMatrix n'est pas
           None, alors on définit l'opérateur "Direct" et "Tangent" à l'aide de
           la matrice, et l'opérateur "Adjoint" à l'aide de la transposée. La
@@ -175,45 +260,93 @@ class AssimilationStudy:
           X divers, l'opérateur par sa valeur appliquée à cet X particulier,
           sous la forme d'un dictionnaire appliedToX[NAME] avec NAME un nom.
           L'opérateur doit néanmoins déjà avoir été défini comme d'habitude.
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
+        L'argument "asFunction" peut prendre la forme complète suivante, avec
+        les valeurs par défaut standards :
+          asFunction = {"Direct":None, "Tangent":None, "Adjoint":None,
+                        "useApproximatedDerivatives":False,
+                        "withCenteredDF"            :False,
+                        "withIncrement"             :0.01,
+                        "withdX"                    :None,
+                        "withAvoidingRedundancy"    :True,
+                        "withToleranceInRedundancy" :1.e-18,
+                        "withLenghtOfRedundancy"    :-1,
+                        "withmpEnabled"             :False,
+                        "withmpWorkers"             :None,
+                       }
         """
-        if (type(asFunction) is type({})) and (asFunction["Tangent"] is not None) and (asFunction["Adjoint"] is not None):
-            if not asFunction.has_key("Direct") or (asFunction["Direct"] is None):
-                self.__H["Direct"]  = Operator( fromMethod = asFunction["Tangent"]  )
+        if isinstance(asFunction, dict) and \
+                ("useApproximatedDerivatives" in asFunction) and bool(asFunction["useApproximatedDerivatives"]) and \
+                ("Direct" in asFunction) and (asFunction["Direct"] is not None):
+            if "withCenteredDF"            not in asFunction: asFunction["withCenteredDF"]            = False
+            if "withIncrement"             not in asFunction: asFunction["withIncrement"]             = 0.01
+            if "withdX"                    not in asFunction: asFunction["withdX"]                    = None
+            if "withAvoidingRedundancy"    not in asFunction: asFunction["withAvoidingRedundancy"]    = True
+            if "withToleranceInRedundancy" not in asFunction: asFunction["withToleranceInRedundancy"] = 1.e-18
+            if "withLenghtOfRedundancy"    not in asFunction: asFunction["withLenghtOfRedundancy"]    = -1
+            if "withmpEnabled"             not in asFunction: asFunction["withmpEnabled"]             = False
+            if "withmpWorkers"             not in asFunction: asFunction["withmpWorkers"]             = None
+            from daNumerics.ApproximatedDerivatives import FDApproximation
+            FDA = FDApproximation(
+                Function              = asFunction["Direct"],
+                centeredDF            = asFunction["withCenteredDF"],
+                increment             = asFunction["withIncrement"],
+                dX                    = asFunction["withdX"],
+                avoidingRedundancy    = asFunction["withAvoidingRedundancy"],
+                toleranceInRedundancy = asFunction["withToleranceInRedundancy"],
+                lenghtOfRedundancy    = asFunction["withLenghtOfRedundancy"],
+                mpEnabled             = asFunction["withmpEnabled"],
+                mpWorkers             = asFunction["withmpWorkers"],
+                )
+            self.__HO["Direct"]  = Operator( fromMethod = FDA.DirectOperator,  avoidingRedundancy = avoidRC )
+            self.__HO["Tangent"] = Operator( fromMethod = FDA.TangentOperator, avoidingRedundancy = avoidRC )
+            self.__HO["Adjoint"] = Operator( fromMethod = FDA.AdjointOperator, avoidingRedundancy = avoidRC )
+        elif isinstance(asFunction, dict) and \
+                ("Tangent" in asFunction) and ("Adjoint" in asFunction) and \
+                (asFunction["Tangent"] is not None) and (asFunction["Adjoint"] is not None):
+            if ("Direct" not in asFunction) or (asFunction["Direct"] is None):
+                self.__HO["Direct"] = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
             else:
-                self.__H["Direct"] = Operator( fromMethod = asFunction["Direct"]  )
-            self.__H["Tangent"]    = Operator( fromMethod = asFunction["Tangent"] )
-            self.__H["Adjoint"]    = Operator( fromMethod = asFunction["Adjoint"] )
+                self.__HO["Direct"] = Operator( fromMethod = asFunction["Direct"],  avoidingRedundancy = avoidRC )
+            self.__HO["Tangent"]    = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
+            self.__HO["Adjoint"]    = Operator( fromMethod = asFunction["Adjoint"], avoidingRedundancy = avoidRC )
         elif asMatrix is not None:
-            mat = numpy.matrix( asMatrix, numpy.float )
-            self.__H["Direct"]  = Operator( fromMatrix = mat )
-            self.__H["Tangent"] = Operator( fromMatrix = mat )
-            self.__H["Adjoint"] = Operator( fromMatrix = mat.T )
+            matrice = numpy.matrix( asMatrix, numpy.float )
+            self.__HO["Direct"]  = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__HO["Tangent"] = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__HO["Adjoint"] = Operator( fromMatrix = matrice.T, avoidingRedundancy = avoidRC )
+            del matrice
         else:
-            raise ValueError("Error: improperly defined observation operator")
+            raise ValueError("Improperly defined observation operator, it requires at minima either a matrix, a Direct for approximate derivatives or a Tangent/Adjoint pair.")
         #
         if appliedToX is not None:
-            self.__H["AppliedToX"] = {}
+            self.__HO["AppliedToX"] = {}
             if type(appliedToX) is not dict:
                 raise ValueError("Error: observation operator defined by \"appliedToX\" need a dictionary as argument.")
             for key in appliedToX.keys():
                 if type( appliedToX[key] ) is type( numpy.matrix([]) ):
                     # Pour le cas où l'on a une vraie matrice
-                    self.__H["AppliedToX"][key] = numpy.matrix( appliedToX[key].A1, numpy.float ).T
+                    self.__HO["AppliedToX"][key] = numpy.matrix( appliedToX[key].A1, numpy.float ).T
                 elif type( appliedToX[key] ) is type( numpy.array([]) ) and len(appliedToX[key].shape) > 1:
                     # Pour le cas où l'on a un vecteur représenté en array avec 2 dimensions
-                    self.__H["AppliedToX"][key] = numpy.matrix( appliedToX[key].reshape(len(appliedToX[key]),), numpy.float ).T
+                    self.__HO["AppliedToX"][key] = numpy.matrix( appliedToX[key].reshape(len(appliedToX[key]),), numpy.float ).T
                 else:
-                    self.__H["AppliedToX"][key] = numpy.matrix( appliedToX[key],    numpy.float ).T
+                    self.__HO["AppliedToX"][key] = numpy.matrix( appliedToX[key],    numpy.float ).T
         else:
-            self.__H["AppliedToX"] = None
+            self.__HO["AppliedToX"] = None
         #
+        if toBeStored:
+            self.__StoredInputs["ObservationOperator"] = self.__HO
         return 0
 
     # -----------------------------------------------------------
     def setEvolutionModel(self,
-            asFunction = {"Direct":None, "Tangent":None, "Adjoint":None},
+            asFunction = None,
             asMatrix   = None,
             Scheduler  = None,
+            toBeStored = False,
+            avoidRC    = True,
             ):
         """
         Permet de définir un opérateur d'évolution M. L'ordre de priorité des
@@ -221,49 +354,258 @@ class AssimilationStudy:
         - si asFunction["Tangent"] et asFunction["Adjoint"] ne sont pas None
           alors on définit l'opérateur à l'aide de fonctions. Si la fonction
           "Direct" n'est pas définie, on prend la fonction "Tangent".
+          Si "useApproximatedDerivatives" est vrai, on utilise une approximation
+          des opérateurs tangents et adjoints. On utilise par défaut des
+          différences finies non centrées ou centrées (si "withCenteredDF" est
+          vrai) avec un incrément multiplicatif "withIncrement" de 1% autour
+          du point courant ou sur le point fixe "withdX".
         - si les fonctions ne sont pas disponibles et si asMatrix n'est pas
           None, alors on définit l'opérateur "Direct" et "Tangent" à l'aide de
           la matrice, et l'opérateur "Adjoint" à l'aide de la transposée. La
           matrice fournie doit être sous une forme compatible avec le
           constructeur de numpy.matrix.
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
+        L'argument "asFunction" peut prendre la forme complète suivante, avec
+        les valeurs par défaut standards :
+          asFunction = {"Direct":None, "Tangent":None, "Adjoint":None,
+                        "useApproximatedDerivatives":False,
+                        "withCenteredDF"            :False,
+                        "withIncrement"             :0.01,
+                        "withdX"                    :None,
+                        "withAvoidingRedundancy"    :True,
+                        "withToleranceInRedundancy" :1.e-18,
+                        "withLenghtOfRedundancy"    :-1,
+                        "withmpEnabled"             :False,
+                        "withmpWorkers"             :None,
+                       }
         """
-        if (type(asFunction) is type({})) and (asFunction["Tangent"] is not None) and (asFunction["Adjoint"] is not None):
-            if not asFunction.has_key("Direct") or (asFunction["Direct"] is None):
-                self.__M["Direct"] = Operator( fromMethod = asFunction["Tangent"]  )
+        if isinstance(asFunction, dict) and \
+                ("useApproximatedDerivatives" in asFunction) and bool(asFunction["useApproximatedDerivatives"]) and \
+                ("Direct" in asFunction) and (asFunction["Direct"] is not None):
+            if "withCenteredDF"            not in asFunction: asFunction["withCenteredDF"]            = False
+            if "withIncrement"             not in asFunction: asFunction["withIncrement"]             = 0.01
+            if "withdX"                    not in asFunction: asFunction["withdX"]                    = None
+            if "withAvoidingRedundancy"    not in asFunction: asFunction["withAvoidingRedundancy"]    = True
+            if "withToleranceInRedundancy" not in asFunction: asFunction["withToleranceInRedundancy"] = 1.e-18
+            if "withLenghtOfRedundancy"    not in asFunction: asFunction["withLenghtOfRedundancy"]    = -1
+            if "withmpEnabled"             not in asFunction: asFunction["withmpEnabled"]             = False
+            if "withmpWorkers"             not in asFunction: asFunction["withmpWorkers"]             = None
+            from daNumerics.ApproximatedDerivatives import FDApproximation
+            FDA = FDApproximation(
+                Function              = asFunction["Direct"],
+                centeredDF            = asFunction["withCenteredDF"],
+                increment             = asFunction["withIncrement"],
+                dX                    = asFunction["withdX"],
+                avoidingRedundancy    = asFunction["withAvoidingRedundancy"],
+                toleranceInRedundancy = asFunction["withToleranceInRedundancy"],
+                lenghtOfRedundancy    = asFunction["withLenghtOfRedundancy"],
+                mpEnabled             = asFunction["withmpEnabled"],
+                mpWorkers             = asFunction["withmpWorkers"],
+                )
+            self.__EM["Direct"]  = Operator( fromMethod = FDA.DirectOperator,  avoidingRedundancy = avoidRC )
+            self.__EM["Tangent"] = Operator( fromMethod = FDA.TangentOperator, avoidingRedundancy = avoidRC )
+            self.__EM["Adjoint"] = Operator( fromMethod = FDA.AdjointOperator, avoidingRedundancy = avoidRC )
+        elif isinstance(asFunction, dict) and \
+                ("Tangent" in asFunction) and ("Adjoint" in asFunction) and \
+                (asFunction["Tangent"] is not None) and (asFunction["Adjoint"] is not None):
+            if ("Direct" not in asFunction) or (asFunction["Direct"] is None):
+                self.__EM["Direct"] = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
             else:
-                self.__M["Direct"] = Operator( fromMethod = asFunction["Direct"]  )
-            self.__M["Tangent"]    = Operator( fromMethod = asFunction["Tangent"] )
-            self.__M["Adjoint"]    = Operator( fromMethod = asFunction["Adjoint"] )
+                self.__EM["Direct"] = Operator( fromMethod = asFunction["Direct"],  avoidingRedundancy = avoidRC )
+            self.__EM["Tangent"]    = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
+            self.__EM["Adjoint"]    = Operator( fromMethod = asFunction["Adjoint"], avoidingRedundancy = avoidRC )
         elif asMatrix is not None:
             matrice = numpy.matrix( asMatrix, numpy.float )
-            self.__M["Direct"]  = Operator( fromMatrix = matrice )
-            self.__M["Tangent"] = Operator( fromMatrix = matrice )
-            self.__M["Adjoint"] = Operator( fromMatrix = matrice.T )
+            self.__EM["Direct"]  = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__EM["Tangent"] = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__EM["Adjoint"] = Operator( fromMatrix = matrice.T, avoidingRedundancy = avoidRC )
+            del matrice
         else:
-            raise ValueError("Error: improperly defined evolution operator")
+            raise ValueError("Improperly defined evolution model, it requires at minima either a matrix, a Direct for approximate derivatives or a Tangent/Adjoint pair.")
+        #
+        if toBeStored:
+            self.__StoredInputs["EvolutionModel"] = self.__EM
         return 0
 
-    def setEvolutionError(self, asCovariance=None):
+    def setEvolutionError(self,
+            asCovariance  = None,
+            asEyeByScalar = None,
+            asEyeByVector = None,
+            asCovObject   = None,
+            toBeStored    = False,
+            toBeChecked   = False,
+            ):
         """
         Permet de définir la covariance des erreurs de modèle :
         - asCovariance : entrée des données, comme une matrice compatible avec
           le constructeur de numpy.matrix
+        - asEyeByScalar : entrée des données comme un seul scalaire de variance,
+          multiplicatif d'une matrice de corrélation identité, aucune matrice
+          n'étant donc explicitement à donner
+        - asEyeByVector : entrée des données comme un seul vecteur de variance,
+          à mettre sur la diagonale d'une matrice de corrélation, aucune matrice
+          n'étant donc explicitement à donner
+        - asCovObject : entrée des données comme un objet ayant des méthodes
+          particulieres de type matriciel
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
-        self.__Q  = numpy.matrix( asCovariance, numpy.float )
+        self.__Q = Covariance(
+            name          = "EvolutionError",
+            asCovariance  = asCovariance,
+            asEyeByScalar = asEyeByScalar,
+            asEyeByVector = asEyeByVector,
+            asCovObject   = asCovObject,
+            toBeChecked   = toBeChecked,
+            )
+        if toBeStored:
+            self.__StoredInputs["EvolutionError"] = self.__Q
         return 0
 
     # -----------------------------------------------------------
-    def setControls (self, asVector = None ):
+    def setControlModel(self,
+            asFunction = {"Direct":None, "Tangent":None, "Adjoint":None,
+                          "useApproximatedDerivatives":False,
+                          "withCenteredDF"            :False,
+                          "withIncrement"             :0.01,
+                          "withdX"                    :None,
+                         },
+            asMatrix   = None,
+            Scheduler  = None,
+            toBeStored = False,
+            avoidRC    = True,
+            ):
+        """
+        Permet de définir un opérateur de controle C. L'ordre de priorité des
+        définitions et leur sens sont les suivants :
+        - si asFunction["Tangent"] et asFunction["Adjoint"] ne sont pas None
+          alors on définit l'opérateur à l'aide de fonctions. Si la fonction
+          "Direct" n'est pas définie, on prend la fonction "Tangent".
+          Si "useApproximatedDerivatives" est vrai, on utilise une approximation
+          des opérateurs tangents et adjoints. On utilise par défaut des
+          différences finies non centrées ou centrées (si "withCenteredDF" est
+          vrai) avec un incrément multiplicatif "withIncrement" de 1% autour
+          du point courant ou sur le point fixe "withdX".
+        - si les fonctions ne sont pas disponibles et si asMatrix n'est pas
+          None, alors on définit l'opérateur "Direct" et "Tangent" à l'aide de
+          la matrice, et l'opérateur "Adjoint" à l'aide de la transposée. La
+          matrice fournie doit être sous une forme compatible avec le
+          constructeur de numpy.matrix.
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
+        L'argument "asFunction" peut prendre la forme complète suivante, avec
+        les valeurs par défaut standards :
+          asFunction = {"Direct":None, "Tangent":None, "Adjoint":None,
+                        "useApproximatedDerivatives":False,
+                        "withCenteredDF"            :False,
+                        "withIncrement"             :0.01,
+                        "withdX"                    :None,
+                        "withAvoidingRedundancy"    :True,
+                        "withToleranceInRedundancy" :1.e-18,
+                        "withLenghtOfRedundancy"    :-1,
+                        "withmpEnabled"             :False,
+                        "withmpWorkers"             :None,
+                       }
+        """
+        if isinstance(asFunction, dict) and \
+                ("useApproximatedDerivatives" in asFunction) and bool(asFunction["useApproximatedDerivatives"]) and \
+                ("Direct" in asFunction) and (asFunction["Direct"] is not None):
+            if "withCenteredDF"            not in asFunction: asFunction["withCenteredDF"]            = False
+            if "withIncrement"             not in asFunction: asFunction["withIncrement"]             = 0.01
+            if "withdX"                    not in asFunction: asFunction["withdX"]                    = None
+            if "withAvoidingRedundancy"    not in asFunction: asFunction["withAvoidingRedundancy"]    = True
+            if "withToleranceInRedundancy" not in asFunction: asFunction["withToleranceInRedundancy"] = 1.e-18
+            if "withLenghtOfRedundancy"    not in asFunction: asFunction["withLenghtOfRedundancy"]    = -1
+            if "withmpEnabled"             not in asFunction: asFunction["withmpEnabled"]             = False
+            if "withmpWorkers"             not in asFunction: asFunction["withmpWorkers"]             = None
+            from daNumerics.ApproximatedDerivatives import FDApproximation
+            FDA = FDApproximation(
+                Function              = asFunction["Direct"],
+                centeredDF            = asFunction["withCenteredDF"],
+                increment             = asFunction["withIncrement"],
+                dX                    = asFunction["withdX"],
+                avoidingRedundancy    = asFunction["withAvoidingRedundancy"],
+                toleranceInRedundancy = asFunction["withToleranceInRedundancy"],
+                lenghtOfRedundancy    = asFunction["withLenghtOfRedundancy"],
+                mpEnabled             = asFunction["withmpEnabled"],
+                mpWorkers             = asFunction["withmpWorkers"],
+                )
+            self.__CM["Direct"]  = Operator( fromMethod = FDA.DirectOperator,  avoidingRedundancy = avoidRC )
+            self.__CM["Tangent"] = Operator( fromMethod = FDA.TangentOperator, avoidingRedundancy = avoidRC )
+            self.__CM["Adjoint"] = Operator( fromMethod = FDA.AdjointOperator, avoidingRedundancy = avoidRC )
+        elif isinstance(asFunction, dict) and \
+                ("Tangent" in asFunction) and ("Adjoint" in asFunction) and \
+                (asFunction["Tangent"] is not None) and (asFunction["Adjoint"] is not None):
+            if ("Direct" not in asFunction) or (asFunction["Direct"] is None):
+                self.__CM["Direct"] = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
+            else:
+                self.__CM["Direct"] = Operator( fromMethod = asFunction["Direct"],  avoidingRedundancy = avoidRC )
+            self.__CM["Tangent"]    = Operator( fromMethod = asFunction["Tangent"], avoidingRedundancy = avoidRC )
+            self.__CM["Adjoint"]    = Operator( fromMethod = asFunction["Adjoint"], avoidingRedundancy = avoidRC )
+        elif asMatrix is not None:
+            matrice = numpy.matrix( asMatrix, numpy.float )
+            self.__CM["Direct"]  = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__CM["Tangent"] = Operator( fromMatrix = matrice,   avoidingRedundancy = avoidRC )
+            self.__CM["Adjoint"] = Operator( fromMatrix = matrice.T, avoidingRedundancy = avoidRC )
+            del matrice
+        else:
+            raise ValueError("Improperly defined input control model, it requires at minima either a matrix, a Direct for approximate derivatives or a Tangent/Adjoint pair.")
+        #
+        if toBeStored:
+            self.__StoredInputs["ControlModel"] = self.__CM
+        return 0
+
+    def setControlInput(self,
+            asVector           = None,
+            asPersistentVector = None,
+            Scheduler          = None,
+            toBeStored         = False,
+            ):
+        """
+        Permet de définir le controle en entree :
+        - asVector : entrée des données, comme un vecteur compatible avec le
+          constructeur de numpy.matrix
+        - asPersistentVector : entrée des données, comme un vecteur de type
+          persistent contruit avec la classe ad-hoc "Persistence"
+        - Scheduler est le contrôle temporel des données disponibles
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
+        """
+        if asVector is not None:
+            self.__U = numpy.matrix( numpy.ravel(numpy.matrix(asVector)), numpy.float ).T
+        elif asPersistentVector is not None:
+            if type(asPersistentVector) in [type([]),type(()),type(numpy.array([])),type(numpy.matrix([]))]:
+                self.__U = Persistence.OneVector("ControlInput", basetype=numpy.matrix)
+                for member in asPersistentVector:
+                    self.__U.store( numpy.matrix( numpy.asmatrix(member).A1, numpy.float ).T )
+            else:
+                self.__U = asPersistentVector
+        else:
+            raise ValueError("Error: improperly defined control input, it requires at minima either a vector, a list/tuple of vectors or a persistent object")
+        if toBeStored:
+            self.__StoredInputs["ControlInput"] = self.__U
+        return 0
+
+    # -----------------------------------------------------------
+    def setControls (self,
+            asVector = None,
+            toBeStored   = False,
+            ):
         """
         Permet de définir la valeur initiale du vecteur X contenant toutes les
         variables de contrôle, i.e. les paramètres ou l'état dont on veut
         estimer la valeur pour obtenir les observations. C'est utile pour un
-        algorithme itératif/incrémental
+        algorithme itératif/incrémental.
         - asVector : entrée des données, comme un vecteur compatible avec le
           constructeur de numpy.matrix.
+        - toBeStored : booléen indiquant si la donnée d'entrée est sauvée pour
+          être rendue disponible au même titre que les variables de calcul
         """
         if asVector is not None:
             self.__X.store( asVector )
+        if toBeStored:
+            self.__StoredInputs["Controls"] = self.__X
         return 0
 
     # -----------------------------------------------------------
@@ -272,7 +614,7 @@ class AssimilationStudy:
         Permet de sélectionner l'algorithme à utiliser pour mener à bien l'étude
         d'assimilation. L'argument est un champ caractère se rapportant au nom
         d'un fichier contenu dans "../daAlgorithms" et réalisant l'opération
-        d'assimilation sur les arguments (Xb,Y,H,R,B,Xa).
+        d'assimilation sur les arguments fixes.
         """
         if choice is None:
             raise ValueError("Error: algorithm choice has to be given")
@@ -302,6 +644,7 @@ class AssimilationStudy:
         # Instancie un objet du type élémentaire du fichier
         # -------------------------------------------------
         self.__algorithm = self.__algorithmFile.ElementaryAlgorithm()
+        self.__StoredInputs["AlgorithmName"] = self.__algorithmName
         return 0
 
     def setAlgorithmParameters(self, asDico=None):
@@ -309,9 +652,18 @@ class AssimilationStudy:
         Permet de définir les paramètres de l'algorithme, sous la forme d'un
         dictionnaire.
         """
-        self.__Parameters = dict( asDico )
+        if asDico is not None:
+            self.__Parameters.update( dict( asDico ) )
+        #
+        self.__StoredInputs["AlgorithmParameters"] = self.__Parameters
         return 0
 
+    def getAlgorithmParameters(self, noDetails=True):
+        """
+        Renvoie la liste des paramètres requis selon l'algorithme
+        """
+        return self.__algorithm.getRequiredParameters(noDetails)
+
     # -----------------------------------------------------------
     def setDiagnostic(self, choice = None, name = "", unit = "", basetype = None, parameters = {} ):
         """
@@ -341,8 +693,10 @@ class AssimilationStudy:
         #
         # Instancie un objet du type élémentaire du fichier
         # -------------------------------------------------
-        if self.__StoredDiagnostics.has_key(name):
-            raise ValueError("A diagnostic with the same name already exists")
+        if name in self.__StoredInputs:
+            raise ValueError("A default input with the same name \"%s\" already exists."%str(name))
+        elif name in self.__StoredDiagnostics:
+            raise ValueError("A diagnostic with the same name \"%s\" already exists."%str(name))
         else:
             self.__StoredDiagnostics[name] = self.__diagnosticFile.ElementaryDiagnostic(
                 name       = name,
@@ -357,83 +711,116 @@ class AssimilationStudy:
         Validation de la correspondance correcte des tailles des variables et
         des matrices s'il y en a.
         """
-        if self.__Xb is None:                  __Xb_shape = (0,)
+        if self.__Xb is None:                      __Xb_shape = (0,)
+        elif hasattr(self.__Xb,"size"):            __Xb_shape = (self.__Xb.size,)
         elif hasattr(self.__Xb,"shape"):
-            if type(self.__Xb.shape) is tuple: __Xb_shape = self.__Xb.shape
-            else:                              __Xb_shape = self.__Xb.shape()
-        else: raise TypeError("Xb has no attribute of shape: problem !")
+            if isinstance(self.__Xb.shape, tuple): __Xb_shape = self.__Xb.shape
+            else:                                  __Xb_shape = self.__Xb.shape()
+        else: raise TypeError("The background (Xb) has no attribute of shape: problem !")
         #
-        if self.__Y is None:                  __Y_shape = (0,)
+        if self.__Y is None:                       __Y_shape = (0,)
+        elif hasattr(self.__Y,"size"):             __Y_shape = (self.__Y.size,)
         elif hasattr(self.__Y,"shape"):
-            if type(self.__Y.shape) is tuple: __Y_shape = self.__Y.shape
-            else:                             __Y_shape = self.__Y.shape()
-        else: raise TypeError("Y has no attribute of shape: problem !")
+            if isinstance(self.__Y.shape, tuple):  __Y_shape = self.__Y.shape
+            else:                                  __Y_shape = self.__Y.shape()
+        else: raise TypeError("The observation (Y) has no attribute of shape: problem !")
         #
-        if self.__B is None:                  __B_shape = (0,0)
+        if self.__U is None:                       __U_shape = (0,)
+        elif hasattr(self.__U,"size"):             __U_shape = (self.__U.size,)
+        elif hasattr(self.__U,"shape"):
+            if isinstance(self.__U.shape, tuple):  __U_shape = self.__U.shape
+            else:                                  __U_shape = self.__U.shape()
+        else: raise TypeError("The control (U) has no attribute of shape: problem !")
+        #
+        if self.__B is None:                       __B_shape = (0,0)
         elif hasattr(self.__B,"shape"):
-            if type(self.__B.shape) is tuple: __B_shape = self.__B.shape
-            else:                             __B_shape = self.__B.shape()
-        else: raise TypeError("B has no attribute of shape: problem !")
+            if isinstance(self.__B.shape, tuple):  __B_shape = self.__B.shape
+            else:                                  __B_shape = self.__B.shape()
+        else: raise TypeError("The a priori errors covariance matrix (B) has no attribute of shape: problem !")
         #
-        if self.__R is None:                  __R_shape = (0,0)
+        if self.__R is None:                       __R_shape = (0,0)
         elif hasattr(self.__R,"shape"):
-            if type(self.__R.shape) is tuple: __R_shape = self.__R.shape
-            else:                             __R_shape = self.__R.shape()
-        else: raise TypeError("R has no attribute of shape: problem !")
+            if isinstance(self.__R.shape, tuple):  __R_shape = self.__R.shape
+            else:                                  __R_shape = self.__R.shape()
+        else: raise TypeError("The observation errors covariance matrix (R) has no attribute of shape: problem !")
         #
-        if self.__Q is None:                  __Q_shape = (0,0)
+        if self.__Q is None:                       __Q_shape = (0,0)
         elif hasattr(self.__Q,"shape"):
-            if type(self.__Q.shape) is tuple: __Q_shape = self.__Q.shape
-            else:                             __Q_shape = self.__Q.shape()
-        else: raise TypeError("Q has no attribute of shape: problem !")
-        #
-        if len(self.__H) == 0:                          __H_shape = (0,0)
-        elif type(self.__H) is type({}):                __H_shape = (0,0)
-        elif hasattr(self.__H["Direct"],"shape"):
-            if type(self.__H["Direct"].shape) is tuple: __H_shape = self.__H["Direct"].shape
-            else:                                       __H_shape = self.__H["Direct"].shape()
-        else: raise TypeError("H has no attribute of shape: problem !")
-        #
-        if len(self.__M) == 0:                          __M_shape = (0,0)
-        elif type(self.__M) is type({}):                __M_shape = (0,0)
-        elif hasattr(self.__M["Direct"],"shape"):
-            if type(self.__M["Direct"].shape) is tuple: __M_shape = self.__M["Direct"].shape
-            else:                                       __M_shape = self.__M["Direct"].shape()
-        else: raise TypeError("M has no attribute of shape: problem !")
+            if isinstance(self.__Q.shape, tuple):  __Q_shape = self.__Q.shape
+            else:                                  __Q_shape = self.__Q.shape()
+        else: raise TypeError("The evolution errors covariance matrix (Q) has no attribute of shape: problem !")
+        #
+        if len(self.__HO) == 0:                              __HO_shape = (0,0)
+        elif isinstance(self.__HO, dict):                    __HO_shape = (0,0)
+        elif hasattr(self.__HO["Direct"],"shape"):
+            if isinstance(self.__HO["Direct"].shape, tuple): __HO_shape = self.__HO["Direct"].shape
+            else:                                            __HO_shape = self.__HO["Direct"].shape()
+        else: raise TypeError("The observation operator (H) has no attribute of shape: problem !")
+        #
+        if len(self.__EM) == 0:                              __EM_shape = (0,0)
+        elif isinstance(self.__EM, dict):                    __EM_shape = (0,0)
+        elif hasattr(self.__EM["Direct"],"shape"):
+            if isinstance(self.__EM["Direct"].shape, tuple): __EM_shape = self.__EM["Direct"].shape
+            else:                                            __EM_shape = self.__EM["Direct"].shape()
+        else: raise TypeError("The evolution model (EM) has no attribute of shape: problem !")
+        #
+        if len(self.__CM) == 0:                              __CM_shape = (0,0)
+        elif isinstance(self.__CM, dict):                    __CM_shape = (0,0)
+        elif hasattr(self.__CM["Direct"],"shape"):
+            if isinstance(self.__CM["Direct"].shape, tuple): __CM_shape = self.__CM["Direct"].shape
+            else:                                            __CM_shape = self.__CM["Direct"].shape()
+        else: raise TypeError("The control model (CM) has no attribute of shape: problem !")
         #
         # Vérification des conditions
         # ---------------------------
         if not( len(__Xb_shape) == 1 or min(__Xb_shape) == 1 ):
-            raise ValueError("Shape characteristic of Xb is incorrect: \"%s\""%(__Xb_shape,))
+            raise ValueError("Shape characteristic of background (Xb) is incorrect: \"%s\"."%(__Xb_shape,))
         if not( len(__Y_shape) == 1 or min(__Y_shape) == 1 ):
-            raise ValueError("Shape characteristic of Y is incorrect: \"%s\""%(__Y_shape,))
+            raise ValueError("Shape characteristic of observation (Y) is incorrect: \"%s\"."%(__Y_shape,))
         #
         if not( min(__B_shape) == max(__B_shape) ):
-            raise ValueError("Shape characteristic of B is incorrect: \"%s\""%(__B_shape,))
+            raise ValueError("Shape characteristic of a priori errors covariance matrix (B) is incorrect: \"%s\"."%(__B_shape,))
         if not( min(__R_shape) == max(__R_shape) ):
-            raise ValueError("Shape characteristic of R is incorrect: \"%s\""%(__R_shape,))
+            raise ValueError("Shape characteristic of observation errors covariance matrix (R) is incorrect: \"%s\"."%(__R_shape,))
         if not( min(__Q_shape) == max(__Q_shape) ):
-            raise ValueError("Shape characteristic of Q is incorrect: \"%s\""%(__Q_shape,))
-        if not( min(__M_shape) == max(__M_shape) ):
-            raise ValueError("Shape characteristic of M is incorrect: \"%s\""%(__M_shape,))
+            raise ValueError("Shape characteristic of evolution errors covariance matrix (Q) is incorrect: \"%s\"."%(__Q_shape,))
+        if not( min(__EM_shape) == max(__EM_shape) ):
+            raise ValueError("Shape characteristic of evolution operator (EM) is incorrect: \"%s\"."%(__EM_shape,))
+        #
+        if len(self.__HO) > 0 and not(type(self.__HO) is type({})) and not( __HO_shape[1] == max(__Xb_shape) ):
+            raise ValueError("Shape characteristic of observation operator (H) \"%s\" and state (X) \"%s\" are incompatible."%(__HO_shape,__Xb_shape))
+        if len(self.__HO) > 0 and not(type(self.__HO) is type({})) and not( __HO_shape[0] == max(__Y_shape) ):
+            raise ValueError("Shape characteristic of observation operator (H) \"%s\" and observation (Y) \"%s\" are incompatible."%(__HO_shape,__Y_shape))
+        if len(self.__HO) > 0 and not(type(self.__HO) is type({})) and len(self.__B) > 0 and not( __HO_shape[1] == __B_shape[0] ):
+            raise ValueError("Shape characteristic of observation operator (H) \"%s\" and a priori errors covariance matrix (B) \"%s\" are incompatible."%(__HO_shape,__B_shape))
+        if len(self.__HO) > 0 and not(type(self.__HO) is type({})) and len(self.__R) > 0 and not( __HO_shape[0] == __R_shape[1] ):
+            raise ValueError("Shape characteristic of observation operator (H) \"%s\" and observation errors covariance matrix (R) \"%s\" are incompatible."%(__HO_shape,__R_shape))
+        #
+        if self.__B is not None and len(self.__B) > 0 and not( __B_shape[1] == max(__Xb_shape) ):
+            if self.__StoredInputs["AlgorithmName"] in ["EnsembleBlue",]:
+                asPersistentVector = self.__Xb.reshape((-1,min(__B_shape)))
+                self.__Xb = Persistence.OneVector("Background", basetype=numpy.matrix)
+                for member in asPersistentVector:
+                    self.__Xb.store( numpy.matrix( numpy.ravel(member), numpy.float ).T )
+                __Xb_shape = min(__B_shape)
+            else:
+                raise ValueError("Shape characteristic of a priori errors covariance matrix (B) \"%s\" and background (Xb) \"%s\" are incompatible."%(__B_shape,__Xb_shape))
         #
-        if len(self.__H) > 0 and not(type(self.__H) is type({})) and not( __H_shape[1] == max(__Xb_shape) ):
-            raise ValueError("Shape characteristic of H \"%s\" and X \"%s\" are incompatible"%(__H_shape,__Xb_shape))
-        if len(self.__H) > 0 and not(type(self.__H) is type({})) and not( __H_shape[0] == max(__Y_shape) ):
-            raise ValueError("Shape characteristic of H \"%s\" and Y \"%s\" are incompatible"%(__H_shape,__Y_shape))
-        if len(self.__H) > 0 and not(type(self.__H) is type({})) and len(self.__B) > 0 and not( __H_shape[1] == __B_shape[0] ):
-            raise ValueError("Shape characteristic of H \"%s\" and B \"%s\" are incompatible"%(__H_shape,__B_shape))
-        if len(self.__H) > 0 and not(type(self.__H) is type({})) and len(self.__R) > 0 and not( __H_shape[0] == __R_shape[1] ):
-            raise ValueError("Shape characteristic of H \"%s\" and R \"%s\" are incompatible"%(__H_shape,__R_shape))
+        if self.__R is not None and len(self.__R) > 0 and not( __R_shape[1] == max(__Y_shape) ):
+            raise ValueError("Shape characteristic of observation errors covariance matrix (R) \"%s\" and observation (Y) \"%s\" are incompatible."%(__R_shape,__Y_shape))
         #
-        if len(self.__B) > 0 and not( __B_shape[1] == max(__Xb_shape) ):
-            raise ValueError("Shape characteristic of B \"%s\" and Xb \"%s\" are incompatible"%(__B_shape,__Xb_shape))
+        if self.__EM is not None and len(self.__EM) > 0 and not(type(self.__EM) is type({})) and not( __EM_shape[1] == max(__Xb_shape) ):
+            raise ValueError("Shape characteristic of evolution model (EM) \"%s\" and state (X) \"%s\" are incompatible."%(__EM_shape,__Xb_shape))
         #
-        if len(self.__R) > 0 and not( __R_shape[1] == max(__Y_shape) ):
-            raise ValueError("Shape characteristic of R \"%s\" and Y \"%s\" are incompatible"%(__R_shape,__Y_shape))
+        if self.__CM is not None and len(self.__CM) > 0 and not(type(self.__CM) is type({})) and not( __CM_shape[1] == max(__U_shape) ):
+            raise ValueError("Shape characteristic of control model (CM) \"%s\" and control (U) \"%s\" are incompatible."%(__CM_shape,__U_shape))
         #
-        if len(self.__M) > 0 and not(type(self.__M) is type({})) and not( __M_shape[1] == max(__Xb_shape) ):
-            raise ValueError("Shape characteristic of M \"%s\" and X \"%s\" are incompatible"%(__M_shape,__Xb_shape))
+        if ("AlgorithmParameters" in self.__StoredInputs) \
+            and ("Bounds" in self.__StoredInputs["AlgorithmParameters"]) \
+            and (isinstance(self.__StoredInputs["AlgorithmParameters"]["Bounds"], list) or isinstance(self.__StoredInputs["AlgorithmParameters"]["Bounds"], tuple)) \
+            and (len(self.__StoredInputs["AlgorithmParameters"]["Bounds"]) != max(__Xb_shape)):
+            raise ValueError("The number \"%s\" of bound pairs for the state (X) components is different of the size \"%s\" of the state itself." \
+                %(len(self.__StoredInputs["AlgorithmParameters"]["Bounds"]),max(__Xb_shape)))
         #
         return 1
 
@@ -441,23 +828,26 @@ class AssimilationStudy:
     def analyze(self):
         """
         Permet de lancer le calcul d'assimilation.
-        
+
         Le nom de la méthode à activer est toujours "run". Les paramètres en
         arguments de la méthode sont fixés. En sortie, on obtient les résultats
         dans la variable de type dictionnaire "StoredVariables", qui contient en
         particulier des objets de Persistence pour les analyses, OMA...
         """
+        Operator.CM.clearCache()
         self.shape_validate()
         #
         self.__algorithm.run(
-            Xb  = self.__Xb,
-            Y   = self.__Y,
-            H   = self.__H,
-            M   = self.__M,
-            R   = self.__R,
-            B   = self.__B,
-            Q   = self.__Q,
-            Par = self.__Parameters,
+            Xb         = self.__Xb,
+            Y          = self.__Y,
+            U          = self.__U,
+            HO         = self.__HO,
+            EM         = self.__EM,
+            CM         = self.__CM,
+            R          = self.__R,
+            B          = self.__B,
+            Q          = self.__Q,
+            Parameters = self.__Parameters,
             )
         return 0
 
@@ -470,36 +860,38 @@ class AssimilationStudy:
         stockée qui est renvoyée, et le diagnostic est inatteignable.
         """
         if key is not None:
-            if self.__algorithm.has_key(key):
+            if key in self.__algorithm:
                 return self.__algorithm.get( key )
-            elif self.__StoredDiagnostics.has_key(key):
+            elif key in self.__StoredInputs:
+                return self.__StoredInputs[key]
+            elif key in self.__StoredDiagnostics:
                 return self.__StoredDiagnostics[key]
             else:
-                raise ValueError("The requested key \"%s\" does not exists as a diagnostic or as a stored variable."%key)
+                raise ValueError("The requested key \"%s\" does not exists as an input, a diagnostic or a stored variable."%key)
         else:
             allvariables = self.__algorithm.get()
             allvariables.update( self.__StoredDiagnostics )
+            allvariables.update( self.__StoredInputs )
             return allvariables
-    
-    def get_available_variables(self, key=None):
+
+    def get_available_variables(self):
         """
         Renvoie les variables potentiellement utilisables pour l'étude,
+        initialement stockées comme données d'entrées ou dans les algorithmes,
         identifiés par les chaînes de caractères. L'algorithme doit avoir été
         préalablement choisi sinon la méthode renvoie "None".
         """
-        
-        if type( self.__algorithm ) is type( {} ):
+        if len( self.__algorithm.keys()) == 0 and len( self.__StoredInputs.keys() ) == 0:
             return None
-        if key is not None:
-            if self.__algorithm.has_key(key):
-                return self.__algorithm.get( key )
-            else:
-                raise ValueError("The requested key \"%s\" does not exists as a stored variable."%key)
         else:
-            variables = self.__algorithm.get().keys()
+            variables = []
+            if len( self.__algorithm.keys()) > 0:
+                variables.extend( self.__algorithm.get().keys() )
+            if len( self.__StoredInputs.keys() ) > 0:
+                variables.extend( self.__StoredInputs.keys() )
             variables.sort()
             return variables
-    
+
     def get_available_algorithms(self):
         """
         Renvoie la liste des algorithmes potentiellement utilisables, identifiés
@@ -514,7 +906,7 @@ class AssimilationStudy:
                         files.append(root)
         files.sort()
         return files
-        
+
     def get_available_diagnostics(self):
         """
         Renvoie la liste des diagnostics potentiellement utilisables, identifiés
@@ -542,7 +934,7 @@ class AssimilationStudy:
         """
         Ajoute au chemin de recherche des algorithmes un répertoire dans lequel
         se trouve un sous-répertoire "daAlgorithms"
-        
+
         Remarque : si le chemin a déjà été ajouté pour les diagnostics, il n'est
         pas indispensable de le rajouter ici.
         """
@@ -553,7 +945,7 @@ class AssimilationStudy:
         if not os.path.isfile(os.path.join(asPath,"daAlgorithms","__init__.py")):
             raise ValueError("The given \""+asPath+"/daAlgorithms\" path must contain a file named \"__init__.py\"")
         sys.path.insert(0, os.path.abspath(asPath))
-        sys.path = list(set(sys.path)) # Conserve en unique exemplaire chaque chemin
+        sys.path = uniq( sys.path ) # Conserve en unique exemplaire chaque chemin
         return 1
 
     def get_diagnostics_main_path(self):
@@ -567,7 +959,7 @@ class AssimilationStudy:
         """
         Ajoute au chemin de recherche des algorithmes un répertoire dans lequel
         se trouve un sous-répertoire "daDiagnostics"
-        
+
         Remarque : si le chemin a déjà été ajouté pour les algorithmes, il n'est
         pas indispensable de le rajouter ici.
         """
@@ -578,7 +970,7 @@ class AssimilationStudy:
         if not os.path.isfile(os.path.join(asPath,"daDiagnostics","__init__.py")):
             raise ValueError("The given \""+asPath+"/daDiagnostics\" path must contain a file named \"__init__.py\"")
         sys.path.insert(0, os.path.abspath(asPath))
-        sys.path = list(set(sys.path)) # Conserve en unique exemplaire chaque chemin
+        sys.path = uniq( sys.path ) # Conserve en unique exemplaire chaque chemin
         return 1
 
     # -----------------------------------------------------------
@@ -589,18 +981,20 @@ class AssimilationStudy:
             Scheduler      = None,
             ):
         """
-        Permet d'associer un observer à une variable nommée gérée en interne,
-        activable selon des règles définies dans le Scheduler.
+        Permet d'associer un observer à une ou des variables nommées gérées en
+        interne, activable selon des règles définies dans le Scheduler. A chaque
+        pas demandé dans le Scheduler, il effectue la fonction HookFunction avec
+        les arguments (variable persistante VariableName, paramètres HookParameters).
         """
-        # 
-        if type( self.__algorithm ) is dict:
+        #
+        if isinstance(self.__algorithm, dict):
             raise ValueError("No observer can be build before choosing an algorithm.")
         #
         # Vérification du nom de variable et typage
         # -----------------------------------------
-        if type( VariableName ) is str:
+        if isinstance(VariableName, str):
             VariableNames = [VariableName,]
-        elif type( VariableName ) is list:
+        elif isinstance(VariableName, list):
             VariableNames = map( str, VariableName )
         else:
             raise ValueError("The observer requires a name or a list of names of variables.")
@@ -608,66 +1002,86 @@ class AssimilationStudy:
         # Association interne de l'observer à la variable
         # -----------------------------------------------
         for n in VariableNames:
-            if not self.__algorithm.has_key( n ):
+            if n not in self.__algorithm:
                 raise ValueError("An observer requires to be set on a variable named %s which does not exist."%n)
+            else:
+                self.__algorithm.StoredVariables[ n ].setDataObserver(
+                    Scheduler      = Scheduler,
+                    HookFunction   = HookFunction,
+                    HookParameters = HookParameters,
+                    )
+
+    def removeDataObserver(self,
+            VariableName   = None,
+            HookFunction   = None,
+            ):
+        """
+        Permet de retirer un observer à une ou des variable nommée.
+        """
+        #
+        if isinstance(self.__algorithm, dict):
+            raise ValueError("No observer can be removed before choosing an algorithm.")
+        #
+        # Vérification du nom de variable et typage
+        # -----------------------------------------
+        if isinstance(VariableName, str):
+            VariableNames = [VariableName,]
+        elif isinstance(VariableName, list):
+            VariableNames = map( str, VariableName )
         else:
-            self.__algorithm.StoredVariables[ n ].setDataObserver(
-                Scheduler      = Scheduler,
-                HookFunction   = HookFunction,
-                HookParameters = HookParameters,
-                )
+            raise ValueError("The observer requires a name or a list of names of variables.")
+        #
+        # Association interne de l'observer à la variable
+        # -----------------------------------------------
+        for n in VariableNames:
+            if n not in self.__algorithm:
+                raise ValueError("An observer requires to be removed on a variable named %s which does not exist."%n)
+            else:
+                self.__algorithm.StoredVariables[ n ].removeDataObserver(
+                    HookFunction   = HookFunction,
+                    )
+
+    # -----------------------------------------------------------
+    def setDebug(self, level=10):
+        """
+        Utiliser par exemple "import logging ; level = logging.DEBUG" avant cet
+        appel pour changer le niveau de verbosité, avec :
+        NOTSET=0 < DEBUG=10 < INFO=20 < WARNING=30 < ERROR=40 < CRITICAL=50
+        """
+        log = logging.getLogger()
+        log.setLevel( level )
+
+    def unsetDebug(self):
+        """
+        Remet le logger au niveau par défaut
+        """
+        log = logging.getLogger()
+        log.setLevel( logging.WARNING )
+
+    def __dir__(self):
+        # return set(self.__dict__.keys() + dir(self.__class__))
+        return ['get', '__doc__', '__init__', '__module__']
+
     def prepare_to_pickle(self):
-      self.__algorithmFile = None
-      self.__diagnosticFile = None
-      self.__H  = {}
+        """
+        Retire les variables non pickelisables
+        """
+        del self.__B
+        del self.__CM # non pickelisable
+        del self.__EM # non pickelisable
+        del self.__HO # non pickelisable
+        del self.__Parameters
+        del self.__Q
+        del self.__R
+        del self.__U
+        del self.__X
+        del self.__Xb
+        del self.__Y
+        del self.__algorithmFile # non pickelisable
+        del self.__algorithmName
+        del self.__diagnosticFile # non pickelisable
+        self.__class__.__doc__ = ""
 
 # ==============================================================================
 if __name__ == "__main__":
     print '\n AUTODIAGNOSTIC \n'
-    
-    ADD = AssimilationStudy("Ma premiere etude BLUE")
-    
-    ADD.setBackground         (asVector     = [0, 1, 2])
-    ADD.setBackgroundError    (asCovariance = "1 0 0;0 1 0;0 0 1")
-    ADD.setObservation        (asVector     = [0.5, 1.5, 2.5])
-    ADD.setObservationError   (asCovariance = "1 0 0;0 1 0;0 0 1")
-    ADD.setObservationOperator(asMatrix     = "1 0 0;0 1 0;0 0 1")
-    
-    ADD.setAlgorithm(choice="Blue")
-    
-    ADD.analyze()
-    
-    print "Nombre d'analyses  :", ADD.get("Analysis").stepnumber()
-    print "Ebauche            :", [0, 1, 2]
-    print "Observation        :", [0.5, 1.5, 2.5]
-    print "Demi-somme         :", list((numpy.array([0, 1, 2])+numpy.array([0.5, 1.5, 2.5]))/2)
-    print "  qui doit être identique à :"
-    print "Analyse résultante :", ADD.get("Analysis").valueserie(0)
-    print "Innovation         :", ADD.get("Innovation").valueserie(0)
-    print
-    
-    print "Algorithmes disponibles :", ADD.get_available_algorithms()
-    # print " Chemin des algorithmes :", ADD.get_algorithms_main_path()
-    print "Diagnostics disponibles :", ADD.get_available_diagnostics()
-    # print " Chemin des diagnostics :", ADD.get_diagnostics_main_path()
-    print
-
-    ADD.setDiagnostic("RMS", "Ma RMS")
-    
-    liste = ADD.get().keys()
-    liste.sort()
-    print "Variables et diagnostics disponibles :", liste
-    print
-
-    def obs(var=None,info=None):
-        print "  ---> Mise en oeuvre de l'observer"
-        print "       var  =",var.valueserie(-1)
-        print "       info =",info
-    ADD.setDataObserver( 'Analysis', HookFunction=obs, Scheduler = [2, 4], HookParameters = "Second observer")
-    # Attention, il faut décaler le stockage de 1 pour suivre le pas interne
-    # car le pas 0 correspond à l'analyse ci-dessus.
-    for i in range(1,6):
-        print
-        print "Action sur la variable observée, étape :",i
-        ADD.get('Analysis').store( [i, i, i] )
-    print