]> SALOME platform Git repositories - modules/adao.git/commitdiff
Salome HOME
Documentation and reporting improvements
authorJean-Philippe ARGAUD <jean-philippe.argaud@edf.fr>
Wed, 8 Sep 2021 05:57:32 +0000 (07:57 +0200)
committerJean-Philippe ARGAUD <jean-philippe.argaud@edf.fr>
Wed, 8 Sep 2021 05:57:32 +0000 (07:57 +0200)
16 files changed:
doc/en/images/schema_temporel_KF.png
doc/en/ref_algorithm_ExtendedKalmanFilter.rst
doc/en/ref_algorithm_KalmanFilter.rst
doc/en/tui.rst
doc/en/tutorials_in_python.rst
doc/fr/images/schema_temporel_KF.png
doc/fr/ref_algorithm_ExtendedKalmanFilter.rst
doc/fr/ref_algorithm_KalmanFilter.rst
doc/fr/tui.rst
doc/fr/tutorials_in_python.rst
src/daComposant/daCore/Aidsm.py
src/daComposant/daCore/BasicObjects.py
src/daComposant/daCore/Interfaces.py
src/daComposant/daCore/PlatformInfo.py
src/daComposant/daCore/Reporting.py [new file with mode: 0644]
src/daComposant/daCore/Templates.py

index a994fd4c60de9b3236cfcb41190a189ef83569e7..dcfa447a7cf26e9e87e5743c3b1fcd238dfd4fd1 100644 (file)
Binary files a/doc/en/images/schema_temporel_KF.png and b/doc/en/images/schema_temporel_KF.png differ
index cc45fe98c9193279e8d0a284e4bd824a3ce32729..900b3fa1008878e551ffe5cbbc6cf34eb886cda2 100644 (file)
@@ -35,8 +35,8 @@ extended Kalman Filter, using a non-linear calculation of the state and the
 incremental evolution (process).
 
 Conceptually, we can represent the temporal pattern of action of the evolution
-operator for this algorithm in the following way, with **x** the state and
-**P** the state error covariance :
+and observation operators in this algorithm in the following way, with **x**
+the state and **P** the state error covariance :
 
   .. _schema_temporel_KF:
   .. image:: images/schema_temporel_KF.png
index f9c5f90d31eaaef1eb036f5d4f9e5a5a08faaffb..13bff48ca08b4de1f9a816dd9b3e4fe600f8ac56 100644 (file)
@@ -39,8 +39,8 @@ cases. One can verify the linearity of the operators with the help of
 the :ref:`section_ref_algorithm_LinearityTest`.
 
 Conceptually, we can represent the temporal pattern of action of the evolution
-operator for this algorithm in the following way, with **x** the state and
-**P** the state error covariance :
+and observation operators in this algorithm in the following way, with **x**
+the state and **P** the state error covariance :
 
   .. _schema_temporel_KF:
   .. image:: images/schema_temporel_KF.png
index 70d78c8c069a0bfeaa08cc4ff1bad882b64fd57f..5125c9b6cbc1598aef6cd1ee012d2dbf9c5285fd 100644 (file)
@@ -627,6 +627,29 @@ with these Python external case operations.
     one the commands establishing the current calculation case. Some formats
     are only available as input or as output.
 
+In addition, simple information about the case study as defined by the user can
+be obtained by using the Python "*print*" command directly on the case, at any
+stage during its design. For example::
+
+    from numpy import array, matrix
+    from adao import adaoBuilder
+    case = adaoBuilder.New()
+    case.set( 'AlgorithmParameters', Algorithm='3DVAR' )
+    case.set( 'Background',          Vector=[0, 1, 2] )
+    print(case)
+
+which result is here::
+
+    ================================================================================
+    ADAO Study report
+    ================================================================================
+
+      - AlgorithmParameters command has been set with values:
+            Algorithm='3DVAR'
+
+      - Background command has been set with values:
+            Vector=[0, 1, 2]
+
 .. _subsection_tui_advanced:
 
 More advanced examples of ADAO TUI calculation case
index 6a268ec899eb63fa62c22e27182256af85c6df38..1adb80bf3104682662f962318374da272cb5e050 100644 (file)
 
 This section presents some examples on using the ADAO module in Python. The
 first one shows how to build a very simple data assimilation case defining
-explicitly all the required input data through the textual user interface
-(TUI). The second one shows, on the same case, how to define input data using
-external sources through scripts. We describe here always Python scripts
-because they can be directly inserted in script definitions of Python
-interface, but external files can use other languages.
+explicitly all the required input data through the textual user interface (TUI)
+described in :ref:`section_tui`. The second one shows, on the same case, how to
+define input data using external sources through scripts. We describe here
+always Python scripts because they can be directly inserted in script
+definitions of Python interface, but external files can use other languages.
 
 These examples are intentionally described in the same way than for the
 :ref:`section_tutorials_in_salome` because they are similar to the ones that
index bd27dde58cdf658cf371109e6a09095aacfde03a..f1a18b5c933b6d8aedc25c0c53ba6c0d186551b8 100644 (file)
Binary files a/doc/fr/images/schema_temporel_KF.png and b/doc/fr/images/schema_temporel_KF.png differ
index 58ab95444493537fb32c7dc24c5ae8810ef9d9fa..e680d49daed61aedd1feadfc47ccf37334a9a15a 100644 (file)
@@ -34,9 +34,9 @@ Cet algorithme réalise une estimation de l'état d'un système dynamique par un
 filtre de Kalman étendu, utilisant un calcul non linéaire de l'état et de
 l'évolution incrémentale (processus).
 
-Conceptuellement, on peut représenter le schéma temporel d'action de
-l'opérateur d'évolution de cet algorithme de la manière suivante, avec **x**
-l'état et **P** la covariance d'erreur d'état :
+Conceptuellement, on peut représenter le schéma temporel d'action des
+opérateurs d'évolution et d'observation dans cet algorithme de la manière
+suivante, avec **x** l'état et **P** la covariance d'erreur d'état :
 
   .. _schema_temporel_KF:
   .. image:: images/schema_temporel_KF.png
index 2c464c60b27a32d586e00dbe6bbffc7ab12446ca..f96dd4c3ed834f8da7e4ede9a093a9042db764e1 100644 (file)
@@ -38,9 +38,9 @@ incrémentale (processus) linéaires, même s'il fonctionne parfois dans les cas
 "faiblement" non-linéaire. On peut vérifier la linéarité de l'opérateur
 d'observation à l'aide de l':ref:`section_ref_algorithm_LinearityTest`.
 
-Conceptuellement, on peut représenter le schéma temporel d'action de
-l'opérateur d'évolution de cet algorithme de la manière suivante, avec **x**
-l'état et **P** la covariance d'erreur d'état :
+Conceptuellement, on peut représenter le schéma temporel d'action des
+opérateurs d'évolution et d'observation dans cet algorithme de la manière
+suivante, avec **x** l'état et **P** la covariance d'erreur d'état :
 
   .. _schema_temporel_KF:
   .. image:: images/schema_temporel_KF.png
index 5b6bd7831c7383fb97ebc663a0dbab4000de47c8..c74dcafde79e4c9ad8d0e3ecf16565443e1e71e7 100644 (file)
@@ -654,6 +654,29 @@ externes au cas.
     autre les commandes établissant le cas de calcul en cours. Certains
     formats ne sont disponibles qu'en entrée ou qu'en sortie.
 
+De plus, on peut obtenir une information simple sur le cas d'étude tel que
+défini par l'utilisateur en utilisant directement la commande "*print*" de Python
+sur le cas, à toute étape lors de sa construction. Par exemple::
+
+    from numpy import array, matrix
+    from adao import adaoBuilder
+    case = adaoBuilder.New()
+    case.set( 'AlgorithmParameters', Algorithm='3DVAR' )
+    case.set( 'Background',          Vector=[0, 1, 2] )
+    print(case)
+
+dont le résultat est ici::
+
+    ================================================================================
+    ADAO Study report
+    ================================================================================
+
+      - AlgorithmParameters command has been set with values:
+            Algorithm='3DVAR'
+
+      - Background command has been set with values:
+            Vector=[0, 1, 2]
+
 .. _subsection_tui_advanced:
 
 Exemples plus avancés de cas de calcul TUI ADAO
index d641a348ba429e9721b5a7edbb59bd6323b92e26..3e08b11783a37a114471c7349084e367ab2db8fa 100644 (file)
 Cette section présente quelques exemples d'utilisation du module ADAO en
 Python. Le premier montre comment construire un cas simple d'assimilation de
 données définissant explicitement toutes les données d'entrée requises à
-travers l'interface utilisateur textuelle (TUI). Le second montre, sur le même
-cas, comment définir les données d'entrée à partir de sources externes à
-travers des scripts. On présente ici toujours des scripts Python car ils sont
-directement insérables dans les définitions de script de l'interface Python,
-mais les fichiers externes peuvent utiliser d'autres langages.
+travers l'interface utilisateur textuelle (TUI) décrite en partie
+:ref:`section_tui`. Le second montre, sur le même cas, comment définir les
+données d'entrée à partir de sources externes à travers des scripts. On
+présente ici toujours des scripts Python car ils sont directement insérables
+dans les définitions de script de l'interface Python, mais les fichiers
+externes peuvent utiliser d'autres langages.
 
 Ces exemples sont intentionnellement décrits de manière semblables aux
 :ref:`section_tutorials_in_salome` car ils sont similaires à ceux que l'on peut
index a981db1b3330e1a1e0e4af1020af85a952e71c4e..679587e02ca33467d85460c636968f6dd135d45b 100644 (file)
@@ -21,7 +21,7 @@
 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
 
 """
-    Normalized interface for ADAO scripting (generic API)
+    Normalized interface for ADAO scripting (generic API).
 """
 __author__ = "Jean-Philippe ARGAUD"
 __all__ = ["Aidsm"]
@@ -816,6 +816,11 @@ class Aidsm(object):
         "Clarifie la visibilité des méthodes"
         return ['set', 'get', 'execute', 'dump', 'load', '__doc__', '__init__', '__module__']
 
+    def __str__(self):
+        "Représentation pour impression (mais pas repr)"
+        msg  = self.dump(None, "SimpleReportInPlainTxt")
+        return msg
+
     def prepare_to_pickle(self):
         "Retire les variables non pickelisables, avec recopie efficace"
         if self.__adaoObject['AlgorithmParameters'] is not None:
index e227aa7aa208464713718d52194face3d2e0ef12..bb0bec8883fd517cd2cdfca899caa1a3d4742b4d 100644 (file)
@@ -2161,7 +2161,7 @@ class Covariance(object):
 # ==============================================================================
 class Observer2Func(object):
     """
-    Creation d'une fonction d'observateur a partir de son texte
+    Création d'une fonction d'observateur a partir de son texte
     """
     def __init__(self, corps=""):
         self.__corps = corps
@@ -2175,7 +2175,7 @@ class Observer2Func(object):
 # ==============================================================================
 class CaseLogger(object):
     """
-    Conservation des commandes de creation d'un cas
+    Conservation des commandes de création d'un cas
     """
     def __init__(self, __name="", __objname="case", __addViewers=None, __addLoaders=None):
         self.__name     = str(__name)
@@ -2186,6 +2186,9 @@ class CaseLogger(object):
             "TUI" :Interfaces._TUIViewer,
             "SCD" :Interfaces._SCDViewer,
             "YACS":Interfaces._YACSViewer,
+            "SimpleReportInRst":Interfaces._SimpleReportInRstViewer,
+            "SimpleReportInHtml":Interfaces._SimpleReportInHtmlViewer,
+            "SimpleReportInPlainTxt":Interfaces._SimpleReportInPlainTxtViewer,
             }
         self.__loaders = {
             "TUI" :Interfaces._TUIViewer,
index c67e3e24ff795f9099766634978fc6ff9dda517a..0b6fbf891a88a7d8d09ea073e59408e6441d6ca8 100644 (file)
@@ -35,6 +35,7 @@ import copy
 from daCore import Persistence
 from daCore import PlatformInfo
 from daCore import Templates
+from daCore import Reporting
 
 # ==============================================================================
 class GenericCaseViewer(object):
@@ -610,6 +611,76 @@ class _YACSViewer(GenericCaseViewer):
         __fid.close()
         return __text
 
+# ==============================================================================
+class _ReportViewer(GenericCaseViewer):
+    """
+    Partie commune de restitution simple
+    """
+    def __init__(self, __name="", __objname="case", __content=None, __object=None):
+        "Initialisation et enregistrement de l'entete"
+        GenericCaseViewer.__init__(self, __name, __objname, __content, __object)
+        self._r = Reporting.ReportStorage()
+        self._r.clear()
+        if self._name == "":
+            self._r.append("ADAO Study report", "title")
+        else:
+            self._r.append(str(self._name), "title")
+        if self._content is not None:
+            for command in self._content:
+                self._append(*command)
+    def _append(self, __command=None, __keys=None, __local=None, __pre=None, __switchoff=False):
+        "Transformation d'une commande individuelle en un enregistrement"
+        if __command is not None and __keys is not None and __local is not None:
+            if __command in ("set","get") and "Concept" in __keys: __command = __local["Concept"]
+            __text  = ""
+            __text += "<i>%s</i> command has been set"%str(__command.replace("set",""))
+            __ktext = ""
+            for k in __keys:
+                if k not in __local: continue
+                __v = __local[k]
+                if __v is None: continue
+                if   k == "Checked"              and not __v: continue
+                if   k == "Stored"               and not __v: continue
+                if   k == "ColMajor"             and not __v: continue
+                if   k == "InputFunctionAsMulti" and not __v: continue
+                if   k == "nextStep"             and not __v: continue
+                if   k == "AvoidRC"              and     __v: continue
+                if   k == "noDetails":                        continue
+                if   k == "Concept":                          continue
+                if   k == "self":                             continue
+                if isinstance(__v,Persistence.Persistence): __v = __v.values()
+                numpy.set_printoptions(precision=15,threshold=1000000,linewidth=1000*15)
+                __ktext += "\n        %s=%s, "%(k,repr(__v))
+                numpy.set_printoptions(precision=8,threshold=1000,linewidth=75)
+            if len(__ktext) > 0:
+                __text += " with values:" + __ktext
+            __text = __text.rstrip(", ")
+            self._r.append(__text, "uli")
+    def _finalize(self, __upa=None):
+        "Enregistrement du final"
+        raise NotImplementedError()
+
+class _SimpleReportInRstViewer(_ReportViewer):
+    """
+    Restitution simple en RST
+    """
+    def _finalize(self, __upa=None):
+        self._lineSerie.append(Reporting.ReportViewInRst(self._r).__str__())
+
+class _SimpleReportInHtmlViewer(_ReportViewer):
+    """
+    Restitution simple en HTML
+    """
+    def _finalize(self, __upa=None):
+        self._lineSerie.append(Reporting.ReportViewInHtml(self._r).__str__())
+
+class _SimpleReportInPlainTxtViewer(_ReportViewer):
+    """
+    Restitution simple en TXT
+    """
+    def _finalize(self, __upa=None):
+        self._lineSerie.append(Reporting.ReportViewInPlainTxt(self._r).__str__())
+
 # ==============================================================================
 class ImportFromScript(object):
     """
index 950e0bbb53fb528dba60d61ebab5bb2a642e3509..9101abe8ebf89f121e8c528be6e508d40ebd903f 100644 (file)
@@ -21,7 +21,7 @@
 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
 
 """
-    Informations sur le code et la plateforme, et mise à jour des chemins
+    Informations sur le code et la plateforme, et mise à jour des chemins.
 
     La classe "PlatformInfo" permet de récupérer les informations générales sur
     le code et la plateforme sous forme de strings, ou d'afficher directement
diff --git a/src/daComposant/daCore/Reporting.py b/src/daComposant/daCore/Reporting.py
new file mode 100644 (file)
index 0000000..96c1cd7
--- /dev/null
@@ -0,0 +1,300 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2021 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 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
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+# Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
+
+"""
+    Gestion simple de rapports structurés créés par l'utilisateur.
+"""
+__author__ = "Jean-Philippe ARGAUD"
+__all__ = []
+
+import os.path
+
+# ==============================================================================
+# Classes de services non utilisateur
+
+class _ReportPartM__(object):
+    """
+    Store and retrieve the data for C: internal class
+    """
+    def __init__(self, part="default"):
+        self.__part    = str(part)
+        self.__styles  = []
+        self.__content = []
+
+    def append(self, content, style="p", position=-1):
+        if position == -1:
+            self.__styles.append(style)
+            self.__content.append(content)
+        else:
+            self.__styles.insert(position, style)
+            self.__content.insert(position, content)
+        return 0
+
+    def get_styles(self):
+        return self.__styles
+
+    def get_content(self):
+        return self.__content
+
+class _ReportM__(object):
+    """
+    Store and retrieve the data for C: internal class
+    """
+    def __init__(self, part='default'):
+        self.__document = {}
+        self.__document[part] = _ReportPartM__(part)
+
+    def append(self,  content, style="p", position=-1, part='default'):
+        if part not in self.__document:
+            self.__document[part] = _ReportPartM__(part)
+        self.__document[part].append(content, style, position)
+        return 0
+
+    def get_styles(self):
+        op = list(self.__document.keys()) ; op.sort()
+        return [self.__document[k].get_styles() for k in op]
+
+    def get_content(self):
+        op = list(self.__document.keys()) ; op.sort()
+        return [self.__document[k].get_content() for k in op]
+
+    def clear(self):
+        self.__init__()
+
+class __ReportC__(object):
+    """
+    Get user commands, update M and V: user intertace to create the report
+    """
+    m = _ReportM__()
+
+    def append(self, content="", style="p", position=-1, part="default"):
+        return self.m.append(content, style, position, part)
+
+    def retrieve(self):
+        st = self.m.get_styles()
+        ct = self.m.get_content()
+        return st, ct
+
+    def clear(self):
+        self.m.clear()
+
+class __ReportV__(object):
+    """
+    Interact with user and C: template for reports
+    """
+
+    default_filename="report.txt"
+
+    def __init__(self, c):
+        self.c = c
+
+    def save(self, filename=None):
+        if filename is None:
+            filename = self.default_filename
+        _filename = os.path.abspath(filename)
+        #
+        h = self.get()
+        fid = open(_filename, 'w')
+        fid.write(h)
+        fid.close()
+        return filename, _filename
+
+    def retrieve(self):
+        return self.c.retrieve()
+
+    def __str__(self):
+        return self.get()
+
+    def close(self):
+        del self.c
+        return 0
+
+# ==============================================================================
+# Classes d'interface utilisateur : ReportStorage, ReportViewIn*
+# Tags de structure : (title, h1, h2, h3, p, uli, oli, <b></b>, <i></i>)
+
+ReportStorage = __ReportC__
+
+class ReportViewInHtml(__ReportV__):
+    """
+    Report in HTML
+    """
+
+    default_filename="report.html"
+    tags = {
+        "oli":"li",
+        "uli":"li",
+        }
+
+    def get(self):
+        st, ct = self.retrieve()
+        inuLi, inoLi = False, False
+        pg = "<html>\n<head>"
+        pg += "\n<title>Report in HTML</title>"
+        pg += "\n</head>\n<body>"
+        for k,ps in enumerate(st):
+            pc = ct[k]
+            try:
+                ii = ps.index("title")
+                title = pc[ii]
+                pg += "%s\n%s\n%s"%('<hr noshade><h1 align="center">',title,'</h1><hr noshade>')
+            except Exception:
+                pass
+            for i,s in enumerate(ps):
+                c = pc[i]
+                if s == "uli" and not inuLi:
+                    pg += "\n<ul>"
+                    inuLi = True
+                elif s == "oli" and not inoLi:
+                    pg += "\n<ol>"
+                    inoLi = True
+                elif s != "uli" and inuLi:
+                    pg += "\n</ul>"
+                    inuLi = False
+                elif s != "oli" and inoLi:
+                    pg += "\n</ol>"
+                    inoLi = False
+                elif s == "title":
+                    continue
+                for t in self.tags:
+                    if s == t: s = self.tags[t]
+                pg += "\n<%s>%s</%s>"%(s,c,s)
+        pg += "\n</body>\n</html>"
+        return pg
+
+class ReportViewInRst(__ReportV__):
+    """
+    Report in RST
+    """
+
+    default_filename="report.rst"
+    tags = {
+        "p":["\n\n",""],
+        "uli":["\n  - ",""],
+        "oli":["\n  #. ",""],
+        }
+    titles = {
+        # "title":["=","="],
+        "h1":["","-"],
+        "h2":["","+"],
+        "h3":["","*"],
+        }
+    translation = {
+        "<b>":"**",
+        "<i>":"*",
+        "</b>":"**",
+        "</i>":"*",
+        }
+
+    def get(self):
+        st, ct = self.retrieve()
+        inuLi, inoLi = False, False
+        pg = ""
+        for k,ps in enumerate(st):
+            pc = ct[k]
+            try:
+                ii = ps.index("title")
+                title = pc[ii]
+                pg += "%s\n%s\n%s"%("="*80,title,"="*80)
+            except Exception:
+                pass
+            for i,s in enumerate(ps):
+                c = pc[i]
+                if s == "uli" and not inuLi:
+                    pg += "\n"
+                    inuLi = True
+                elif s == "oli" and not inoLi:
+                    pg += "\n"
+                    inoLi = True
+                elif s != "uli" and inuLi:
+                    pg += "\n"
+                    inuLi = False
+                elif s != "oli" and inoLi:
+                    pg += "\n"
+                    inoLi = False
+                for t in self.translation:
+                    c = c.replace(t,self.translation[t])
+                if s in self.titles.keys():
+                    pg += "\n%s\n%s\n%s"%(self.titles[s][0]*len(c),c,self.titles[s][1]*len(c))
+                elif s in self.tags.keys():
+                    pg += "%s%s%s"%(self.tags[s][0],c,self.tags[s][1])
+            pg += "\n"
+        return pg
+
+class ReportViewInPlainTxt(__ReportV__):
+    """
+    Report in plain TXT
+    """
+
+    default_filename="report.txt"
+    tags = {
+        "p":["\n",""],
+        "uli":["\n  - ",""],
+        "oli":["\n  - ",""],
+        }
+    titles = {
+        # "title":["=","="],
+        "h1":["",""],
+        "h2":["",""],
+        "h3":["",""],
+        }
+    translation = {
+        "<b>":"",
+        "<i>":"",
+        "</b>":"",
+        "</i>":"",
+        }
+
+    def get(self):
+        st, ct = self.retrieve()
+        inuLi, inoLi = False, False
+        pg = ""
+        for k,ps in enumerate(st):
+            pc = ct[k]
+            try:
+                ii = ps.index("title")
+                title = pc[ii]
+                pg += "%s\n%s\n%s"%("="*80,title,"="*80)
+            except Exception:
+                pass
+            for i,s in enumerate(ps):
+                c = pc[i]
+                if s == "uli" and not inuLi:
+                    inuLi = True
+                elif s == "oli" and not inoLi:
+                    inoLi = True
+                elif s != "uli" and inuLi:
+                    inuLi = False
+                elif s != "oli" and inoLi:
+                    inoLi = False
+                for t in self.translation:
+                    c = c.replace(t,self.translation[t])
+                if s in self.titles.keys():
+                    pg += "\n%s\n%s\n%s"%(self.titles[s][0]*len(c),c,self.titles[s][1]*len(c))
+                elif s in self.tags.keys():
+                    pg += "\n%s%s%s"%(self.tags[s][0],c,self.tags[s][1])
+            pg += "\n"
+        return pg
+
+# ==============================================================================
+if __name__ == "__main__":
+    print('\n AUTODIAGNOSTIC\n')
index cb3f0a273e1d06c65b2baa1d0345639c1eaad5ae..7f9e9f555fd3eb0fa469a2c6511ceb47c40650a1 100644 (file)
@@ -21,7 +21,7 @@
 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
 
 """
-    Modèles généraux pour les observers, le post-processing
+    Modèles généraux pour les observers, le post-processing.
 """
 __author__ = "Jean-Philippe ARGAUD"
 __all__ = ["ObserverTemplates"]