Salome HOME
Minor source update for OM compatibility
[modules/adao.git] / src / daComposant / daCore / Persistence.py
index 43c18965cd6860bcd12bba63770886e998609e75..29d7f71f5e961810f2468e8f9cd0cbc585ee3e9d 100644 (file)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2008-2018 EDF R&D
+# Copyright (C) 2008-2024 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
 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
 
 """
-    Définit des outils de persistence et d'enregistrement de séries de valeurs
+    Définit des outils de persistance et d'enregistrement de séries de valeurs
     pour analyse ultérieure ou utilisation de calcul.
 """
 __author__ = "Jean-Philippe ARGAUD"
 __all__ = []
 
-import os, sys, numpy, copy
-import gzip, bz2
+import os, numpy, copy, math
+import gzip, bz2, pickle
 
-from daCore.PlatformInfo import PathManagement ; PathManagement()
-from daCore.PlatformInfo import has_gnuplot
-if has_gnuplot:
-    import Gnuplot
+from daCore.PlatformInfo import PathManagement ; PathManagement()  # noqa: E702,E203
+from daCore.PlatformInfo import PlatformInfo
+lpi = PlatformInfo()
+mfp = lpi.MaximumPrecision()
 
-if sys.version_info.major < 3:
-    range = xrange
-    iLong = long
-    import cPickle as pickle
-else:
-    iLong = int
-    import pickle
+if lpi.has_gnuplot:
+    import Gnuplot
 
 # ==============================================================================
 class Persistence(object):
     """
-    Classe générale de persistence définissant les accesseurs nécessaires
+    Classe générale de persistance définissant les accesseurs nécessaires
     (Template)
     """
+    __slots__ = (
+        "__name", "__unit", "__basetype", "__values", "__tags", "__dynamic",
+        "__g", "__title", "__ltitle", "__pause", "__dataobservers",
+    )
+
     def __init__(self, name="", unit="", basetype=str):
         """
         name : nom courant
@@ -91,20 +91,36 @@ class Persistence(object):
         """
         Stocke une valeur avec ses informations de filtrage.
         """
-        if value is None: raise ValueError("Value argument required")
+        if value is None:
+            raise ValueError("Value argument required")
         #
         self.__values.append(copy.copy(self.__basetype(value)))
         self.__tags.append(kwargs)
         #
-        if self.__dynamic: self.__replots()
+        if self.__dynamic:
+            self.__replots()
         __step = len(self.__values) - 1
-        for hook, parameters, scheduler in self.__dataobservers:
+        for hook, parameters, scheduler, order, osync, dovar in self.__dataobservers:
             if __step in scheduler:
-                hook( self, parameters )
+                if order is None or dovar is None:
+                    hook( self, parameters )
+                else:
+                    if not isinstance(order, (list, tuple)):
+                        continue
+                    if not isinstance(dovar, dict):
+                        continue
+                    if not bool(osync):  # Async observation
+                        hook( self, parameters, order, dovar )
+                    else:  # Sync observations
+                        for v in order:
+                            if len(dovar[v]) != len(self):
+                                break
+                        else:
+                            hook( self, parameters, order, dovar )
 
     def pop(self, item=None):
         """
-        Retire une valeur enregistree par son index de stockage. Sans argument,
+        Retire une valeur enregistrée par son index de stockage. Sans argument,
         retire le dernier objet enregistre.
         """
         if item is not None:
@@ -138,14 +154,17 @@ class Persistence(object):
     def __str__(self):
         "x.__str__() <==> str(x)"
         msg  = "   Index        Value   Tags\n"
-        for i,v in enumerate(self.__values):
-            msg += "  i=%05i  %10s   %s\n"%(i,v,self.__tags[i])
+        for iv, vv in enumerate(self.__values):
+            msg += "  i=%05i  %10s   %s\n"%(iv, vv, self.__tags[iv])
         return msg
 
     def __len__(self):
         "x.__len__() <==> len(x)"
         return len(self.__values)
 
+    def name(self):
+        return self.__name
+
     def __getitem__(self, index=None ):
         "x.__getitem__(y) <==> x[y]"
         return copy.copy(self.__values[index])
@@ -156,7 +175,8 @@ class Persistence(object):
 
     def index(self, value, start=0, stop=None):
         "L.index(value, [start, [stop]]) -> integer -- return first index of value."
-        if stop is None : stop = len(self.__values)
+        if stop is None:
+            stop = len(self.__values)
         return self.__values.index(value, start, stop)
 
     # ---------------------------------------------------------
@@ -171,10 +191,11 @@ class Persistence(object):
                     if tagKey in self.__tags[i]:
                         if self.__tags[i][tagKey] == kwargs[tagKey]:
                             __tmp.append( i )
-                        elif isinstance(kwargs[tagKey],(list,tuple)) and self.__tags[i][tagKey] in kwargs[tagKey]:
+                        elif isinstance(kwargs[tagKey], (list, tuple)) and self.__tags[i][tagKey] in kwargs[tagKey]:
                             __tmp.append( i )
                 __indexOfFilteredItems = __tmp
-                if len(__indexOfFilteredItems) == 0: break
+                if len(__indexOfFilteredItems) == 0:
+                    break
         return __indexOfFilteredItems
 
     # ---------------------------------------------------------
@@ -183,7 +204,7 @@ class Persistence(object):
         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
         return [self.__values[i] for i in __indexOfFilteredItems]
 
-    def keys(self, keyword=None , **kwargs):
+    def keys(self, keyword=None, **kwargs):
         "D.keys() -> list of D's keys"
         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
         __keys = []
@@ -194,7 +215,7 @@ class Persistence(object):
                 __keys.append( None )
         return __keys
 
-    def items(self, keyword=None , **kwargs):
+    def items(self, keyword=None, **kwargs):
         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
         __pairs = []
@@ -231,7 +252,7 @@ class Persistence(object):
             __indexOfFilteredItems = [item,]
         #
         # Dans le cas où la sortie donne les valeurs d'un "outputTag"
-        if outputTag is not None and isinstance(outputTag,str) :
+        if outputTag is not None and isinstance(outputTag, str):
             outputValues = []
             for index in __indexOfFilteredItems:
                 if outputTag in self.__tags[index].keys():
@@ -251,18 +272,18 @@ class Persistence(object):
                 return allKeys
 
     # ---------------------------------------------------------
-    # Pour compatibilite
+    # Pour compatibilité
     def stepnumber(self):
         "Nombre de pas"
         return len(self.__values)
 
-    # Pour compatibilite
+    # Pour compatibilité
     def stepserie(self, **kwargs):
         "Nombre de pas filtrés"
         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
         return __indexOfFilteredItems
 
-    # Pour compatibilite
+    # Pour compatibilité
     def steplist(self, **kwargs):
         "Nombre de pas filtrés"
         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
@@ -271,18 +292,19 @@ class Persistence(object):
     # ---------------------------------------------------------
     def means(self):
         """
-        Renvoie la série, contenant à chaque pas, la valeur moyenne des données
+        Renvoie la série contenant, à chaque pas, la valeur moyenne des données
         au pas. Il faut que le type de base soit compatible avec les types
         élémentaires numpy.
         """
         try:
-            return [numpy.matrix(item).mean() for item in self.__values]
-        except:
+            __sr = [numpy.mean(item, dtype=mfp).astype('float') for item in self.__values]
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
 
     def stds(self, ddof=0):
         """
-        Renvoie la série, contenant à chaque pas, l'écart-type des données
+        Renvoie la série contenant, à chaque pas, l'écart-type des données
         au pas. Il faut que le type de base soit compatible avec les types
         élémentaires numpy.
 
@@ -291,44 +313,180 @@ class Persistence(object):
         """
         try:
             if numpy.version.version >= '1.1.0':
-                return [numpy.matrix(item).std(ddof=ddof) for item in self.__values]
+                __sr = [numpy.array(item).std(ddof=ddof, dtype=mfp).astype('float') for item in self.__values]
             else:
-                return [numpy.matrix(item).std() for item in self.__values]
-        except:
+                return [numpy.array(item).std(dtype=mfp).astype('float') for item in self.__values]
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
 
     def sums(self):
         """
-        Renvoie la série, contenant à chaque pas, la somme des données au pas.
+        Renvoie la série contenant, à chaque pas, la somme des données au pas.
         Il faut que le type de base soit compatible avec les types élémentaires
         numpy.
         """
         try:
-            return [numpy.matrix(item).sum() for item in self.__values]
-        except:
+            __sr = [numpy.array(item).sum() for item in self.__values]
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
 
     def mins(self):
         """
-        Renvoie la série, contenant à chaque pas, le minimum des données au pas.
+        Renvoie la série contenant, à chaque pas, le minimum des données au pas.
         Il faut que le type de base soit compatible avec les types élémentaires
         numpy.
         """
         try:
-            return [numpy.matrix(item).min() for item in self.__values]
-        except:
+            __sr = [numpy.array(item).min() for item in self.__values]
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
 
     def maxs(self):
         """
-        Renvoie la série, contenant à chaque pas, la maximum des données au pas.
+        Renvoie la série contenant, à chaque pas, la maximum des données au pas.
+        Il faut que le type de base soit compatible avec les types élémentaires
+        numpy.
+        """
+        try:
+            __sr = [numpy.array(item).max() for item in self.__values]
+        except Exception:
+            raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    def powers(self, x2):
+        """
+        Renvoie la série contenant, à chaque pas, la puissance "**x2" au pas.
+        Il faut que le type de base soit compatible avec les types élémentaires
+        numpy.
+        """
+        try:
+            __sr = [numpy.power(item, x2) for item in self.__values]
+        except Exception:
+            raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    def norms(self, _ord=None):
+        """
+        Norm (_ord : voir numpy.linalg.norm)
+
+        Renvoie la série contenant, à chaque pas, la norme des données au pas.
         Il faut que le type de base soit compatible avec les types élémentaires
         numpy.
         """
         try:
-            return [numpy.matrix(item).max() for item in self.__values]
-        except:
+            __sr = [numpy.linalg.norm(item, _ord) for item in self.__values]
+        except Exception:
+            raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    def traces(self, offset=0):
+        """
+        Trace
+
+        Renvoie la série contenant, à chaque pas, la trace (avec l'offset) des
+        données au pas. Il faut que le type de base soit compatible avec les
+        types élémentaires numpy.
+        """
+        try:
+            __sr = [numpy.trace(item, offset, dtype=mfp).astype('float') for item in self.__values]
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    def maes(self, _predictor=None):
+        """
+        Mean Absolute Error (MAE)
+        mae(dX) = 1/n sum(dX_i)
+
+        Renvoie la série contenant, à chaque pas, la MAE des données au pas.
+        Il faut que le type de base soit compatible avec les types élémentaires
+        numpy. C'est réservé aux variables d'écarts ou d'incréments si le
+        prédicteur est None, sinon c'est appliqué à l'écart entre les données
+        au pas et le prédicteur au même pas.
+        """
+        if _predictor is None:
+            try:
+                __sr = [numpy.mean(numpy.abs(item)) for item in self.__values]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        else:
+            if len(_predictor) != len(self.__values):
+                raise ValueError("Predictor number of steps is incompatible with the values")
+            for i, item in enumerate(self.__values):
+                if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
+                    raise ValueError("Predictor size at step %i is incompatible with the values"%i)
+            try:
+                __sr = [numpy.mean(numpy.abs(numpy.ravel(item) - numpy.ravel(_predictor[i]))) for i, item in enumerate(self.__values)]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    def mses(self, _predictor=None):
+        """
+        Mean-Square Error (MSE) ou Mean-Square Deviation (MSD)
+        mse(dX) = 1/n sum(dX_i**2)
+
+        Renvoie la série contenant, à chaque pas, la MSE des données au pas. Il
+        faut que le type de base soit compatible avec les types élémentaires
+        numpy. C'est réservé aux variables d'écarts ou d'incréments si le
+        prédicteur est None, sinon c'est appliqué à l'écart entre les données
+        au pas et le prédicteur au même pas.
+        """
+        if _predictor is None:
+            try:
+                __n = self.shape()[0]
+                __sr = [(numpy.linalg.norm(item)**2 / __n) for item in self.__values]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        else:
+            if len(_predictor) != len(self.__values):
+                raise ValueError("Predictor number of steps is incompatible with the values")
+            for i, item in enumerate(self.__values):
+                if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
+                    raise ValueError("Predictor size at step %i is incompatible with the values"%i)
+            try:
+                __n = self.shape()[0]
+                __sr = [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i]))**2 / __n) for i, item in enumerate(self.__values)]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    msds = mses  # Mean-Square Deviation (MSD=MSE)
+
+    def rmses(self, _predictor=None):
+        """
+        Root-Mean-Square Error (RMSE) ou Root-Mean-Square Deviation (RMSD)
+        rmse(dX) = sqrt( 1/n sum(dX_i**2) ) = sqrt( mse(dX) )
+
+        Renvoie la série contenant, à chaque pas, la RMSE des données au pas.
+        Il faut que le type de base soit compatible avec les types élémentaires
+        numpy. C'est réservé aux variables d'écarts ou d'incréments si le
+        prédicteur est None, sinon c'est appliqué à l'écart entre les données
+        au pas et le prédicteur au même pas.
+        """
+        if _predictor is None:
+            try:
+                __n = self.shape()[0]
+                __sr = [(numpy.linalg.norm(item) / math.sqrt(__n)) for item in self.__values]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        else:
+            if len(_predictor) != len(self.__values):
+                raise ValueError("Predictor number of steps is incompatible with the values")
+            for i, item in enumerate(self.__values):
+                if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
+                    raise ValueError("Predictor size at step %i is incompatible with the values"%i)
+            try:
+                __n = self.shape()[0]
+                __sr = [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i])) / math.sqrt(__n)) for i, item in enumerate(self.__values)]
+            except Exception:
+                raise TypeError("Base type is incompatible with numpy")
+        return numpy.array(__sr).tolist()
+
+    rmsds = rmses  # Root-Mean-Square Deviation (RMSD=RMSE)
 
     def __preplots(self,
                    title    = "",
@@ -337,28 +495,29 @@ class Persistence(object):
                    ltitle   = None,
                    geometry = "600x400",
                    persist  = False,
-                   pause    = True,
-                  ):
+                   pause    = True ):
         "Préparation des plots"
         #
         # Vérification de la disponibilité du module Gnuplot
-        if not has_gnuplot:
+        if not lpi.has_gnuplot:
             raise ImportError("The Gnuplot module is required to plot the object.")
         #
         # Vérification et compléments sur les paramètres d'entrée
-        if persist:
-            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist -geometry '+geometry
-        else:
-            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -geometry '+geometry
         if ltitle is None:
             ltitle = ""
-        self.__g = Gnuplot.Gnuplot() # persist=1
-        self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term)
+        __geometry = str(geometry)
+        __sizespec = (__geometry.split('+')[0]).replace('x', ',')
+        #
+        if persist:
+            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
+        #
+        self.__g = Gnuplot.Gnuplot()  # persist=1
+        self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
         self.__g('set style data lines')
         self.__g('set grid')
         self.__g('set autoscale')
-        self.__g('set xlabel "'+str(xlabel)+'"')
-        self.__g('set ylabel "'+str(ylabel)+'"')
+        self.__g('set xlabel "' + str(xlabel) + '"')
+        self.__g('set ylabel "' + str(ylabel) + '"')
         self.__title  = title
         self.__ltitle = ltitle
         self.__pause  = pause
@@ -375,8 +534,7 @@ class Persistence(object):
               filename = "",
               dynamic  = False,
               persist  = False,
-              pause    = True,
-             ):
+              pause    = True ):
         """
         Renvoie un affichage de la valeur à chaque pas, si elle est compatible
         avec un affichage Gnuplot (donc essentiellement un vecteur). Si
@@ -416,7 +574,8 @@ class Persistence(object):
             self.__preplots(title, xlabel, ylabel, ltitle, geometry, persist, pause )
             if dynamic:
                 self.__dynamic = True
-                if len(self.__values) == 0: return 0
+                if len(self.__values) == 0:
+                    return 0
         #
         # Tracé du ou des vecteurs demandés
         indexes = []
@@ -429,8 +588,8 @@ class Persistence(object):
         #
         i = -1
         for index in indexes:
-            self.__g('set title  "'+str(title)+' (pas '+str(index)+')"')
-            if isinstance(steps,list) or isinstance(steps,numpy.ndarray):
+            self.__g('set title  "' + str(title) + ' (pas ' + str(index) + ')"')
+            if isinstance(steps, (list, numpy.ndarray)):
                 Steps = list(steps)
             else:
                 Steps = list(range(len(self.__values[index])))
@@ -439,7 +598,7 @@ class Persistence(object):
             #
             if filename != "":
                 i += 1
-                stepfilename = "%s_%03i.ps"%(filename,i)
+                stepfilename = "%s_%03i.ps"%(filename, i)
                 if os.path.isfile(stepfilename):
                     raise ValueError("Error: a file with this name \"%s\" already exists."%stepfilename)
                 self.__g.hardcopy(filename=stepfilename, color=1)
@@ -450,9 +609,10 @@ class Persistence(object):
         """
         Affichage dans le cas du suivi dynamique de la variable
         """
-        if self.__dynamic and len(self.__values) < 2: return 0
+        if self.__dynamic and len(self.__values) < 2:
+            return 0
         #
-        self.__g('set title  "'+str(self.__title))
+        self.__g('set title  "' + str(self.__title))
         Steps = list(range(len(self.__values)))
         self.__g.plot( Gnuplot.Data( Steps, self.__values, title=self.__ltitle ) )
         #
@@ -460,6 +620,7 @@ class Persistence(object):
             eval(input('Please press return to continue...\n'))
 
     # ---------------------------------------------------------
+    # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
     def mean(self):
         """
         Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
@@ -467,11 +628,8 @@ class Persistence(object):
         les types élémentaires numpy.
         """
         try:
-            if self.__basetype in [int, float]:
-                return float( numpy.array(self.__values).mean() )
-            else:
-                return numpy.array(self.__values).mean(axis=0)
-        except:
+            return numpy.mean(self.__values, axis=0, dtype=mfp).astype('float')
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
     def std(self, ddof=0):
@@ -485,10 +643,10 @@ class Persistence(object):
         """
         try:
             if numpy.version.version >= '1.1.0':
-                return numpy.array(self.__values).std(ddof=ddof,axis=0)
+                return numpy.asarray(self.__values).std(ddof=ddof, axis=0).astype('float')
             else:
-                return numpy.array(self.__values).std(axis=0)
-        except:
+                return numpy.asarray(self.__values).std(axis=0).astype('float')
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
     def sum(self):
@@ -498,8 +656,8 @@ class Persistence(object):
         les types élémentaires numpy.
         """
         try:
-            return numpy.array(self.__values).sum(axis=0)
-        except:
+            return numpy.asarray(self.__values).sum(axis=0)
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
     def min(self):
@@ -509,8 +667,8 @@ class Persistence(object):
         les types élémentaires numpy.
         """
         try:
-            return numpy.array(self.__values).min(axis=0)
-        except:
+            return numpy.asarray(self.__values).min(axis=0)
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
     def max(self):
@@ -520,8 +678,8 @@ class Persistence(object):
         les types élémentaires numpy.
         """
         try:
-            return numpy.array(self.__values).max(axis=0)
-        except:
+            return numpy.asarray(self.__values).max(axis=0)
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
     def cumsum(self):
@@ -531,13 +689,10 @@ class Persistence(object):
         les types élémentaires numpy.
         """
         try:
-            return numpy.array(self.__values).cumsum(axis=0)
-        except:
+            return numpy.asarray(self.__values).cumsum(axis=0)
+        except Exception:
             raise TypeError("Base type is incompatible with numpy")
 
-    # On pourrait aussi utiliser les autres attributs d'une "matrix", comme
-    # "tofile", "min"...
-
     def plot(self,
              steps    = None,
              title    = "",
@@ -547,8 +702,7 @@ class Persistence(object):
              geometry = "600x400",
              filename = "",
              persist  = False,
-             pause    = True,
-            ):
+             pause    = True ):
         """
         Renvoie un affichage unique pour l'ensemble des valeurs à chaque pas, si
         elles sont compatibles avec un affichage Gnuplot (donc essentiellement
@@ -577,34 +731,36 @@ class Persistence(object):
         """
         #
         # Vérification de la disponibilité du module Gnuplot
-        if not has_gnuplot:
+        if not lpi.has_gnuplot:
             raise ImportError("The Gnuplot module is required to plot the object.")
         #
         # Vérification et compléments sur les paramètres d'entrée
-        if persist:
-            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist -geometry '+geometry
-        else:
-            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -geometry '+geometry
         if ltitle is None:
             ltitle = ""
-        if isinstance(steps,list) or isinstance(steps, numpy.ndarray):
+        if isinstance(steps, (list, numpy.ndarray)):
             Steps = list(steps)
         else:
             Steps = list(range(len(self.__values[0])))
-        self.__g = Gnuplot.Gnuplot() # persist=1
-        self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term)
+        __geometry = str(geometry)
+        __sizespec = (__geometry.split('+')[0]).replace('x', ',')
+        #
+        if persist:
+            Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
+        #
+        self.__g = Gnuplot.Gnuplot()  # persist=1
+        self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
         self.__g('set style data lines')
         self.__g('set grid')
         self.__g('set autoscale')
-        self.__g('set title  "'+str(title) +'"')
-        self.__g('set xlabel "'+str(xlabel)+'"')
-        self.__g('set ylabel "'+str(ylabel)+'"')
+        self.__g('set title  "' + str(title)  + '"')
+        self.__g('set xlabel "' + str(xlabel) + '"')
+        self.__g('set ylabel "' + str(ylabel) + '"')
         #
         # Tracé du ou des vecteurs demandés
         indexes = list(range(len(self.__values)))
-        self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle+" (pas 0)" ) )
+        self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle + " (pas 0)" ) )
         for index in indexes:
-            self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle+" (pas %i)"%index ) )
+            self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle + " (pas %i)"%index ) )
         #
         if filename != "":
             self.__g.hardcopy(filename=filename, color=1)
@@ -612,55 +768,79 @@ class Persistence(object):
             eval(input('Please press return to continue...\n'))
 
     # ---------------------------------------------------------
-    def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None):
+    def s2mvr(self):
+        """
+        Renvoie la série sous la forme d'une unique matrice avec les données au
+        pas rangées par ligne
+        """
+        try:
+            return numpy.asarray(self.__values)
+        except Exception:
+            raise TypeError("Base type is incompatible with numpy")
+
+    def s2mvc(self):
+        """
+        Renvoie la série sous la forme d'une unique matrice avec les données au
+        pas rangées par colonne
         """
-        Association à la variable d'un triplet définissant un observer
+        try:
+            return numpy.asarray(self.__values).transpose()
+            # Eqvlt: return numpy.stack([numpy.ravel(sv) for sv in self.__values], axis=1)
+        except Exception:
+            raise TypeError("Base type is incompatible with numpy")
 
-        Le Scheduler attendu est une fréquence, une simple liste d'index ou un
-        range des index.
+    # ---------------------------------------------------------
+    def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None, Order = None, OSync = True, DOVar = None):
+        """
+        Association à la variable d'un triplet définissant un observer.
+
+        Les variables Order et DOVar sont utilisées pour un observer
+        multi-variable. Le Scheduler attendu est une fréquence, une simple
+        liste d'index ou un range des index.
         """
         #
         # Vérification du Scheduler
         # -------------------------
         maxiter = int( 1e9 )
-        if isinstance(Scheduler,int):      # Considéré comme une fréquence à partir de 0
+        if isinstance(Scheduler, int):                # Considéré comme une fréquence à partir de 0
             Schedulers = range( 0, maxiter, int(Scheduler) )
-        elif isinstance(Scheduler,range):  # Considéré comme un itérateur
+        elif isinstance(Scheduler, range):            # Considéré comme un itérateur
             Schedulers = Scheduler
-        elif isinstance(Scheduler,(list,tuple)):   # Considéré comme des index explicites
-            Schedulers = [iLong(i) for i in Scheduler] # map( long, Scheduler )
-        else:                              # Dans tous les autres cas, activé par défaut
+        elif isinstance(Scheduler, (list, tuple)):    # Considéré comme des index explicites
+            Schedulers = [int(i) for i in Scheduler]  # Similaire à map( int, Scheduler )  # noqa: E262
+        else:                                         # Dans tous les autres cas, activé par défaut
             Schedulers = range( 0, maxiter )
         #
         # Stockage interne de l'observer dans la variable
         # -----------------------------------------------
-        self.__dataobservers.append( [HookFunction, HookParameters, Schedulers] )
+        self.__dataobservers.append( [HookFunction, HookParameters, Schedulers, Order, OSync, DOVar] )
 
     def removeDataObserver(self, HookFunction = None, AllObservers = False):
         """
         Suppression d'un observer nommé sur la variable.
 
-        On peut donner dans HookFunction la meme fonction que lors de la
+        On peut donner dans HookFunction la même fonction que lors de la
         définition, ou un simple string qui est le nom de la fonction. Si
         AllObservers est vrai, supprime tous les observers enregistrés.
         """
-        if hasattr(HookFunction,"func_name"):
+        if hasattr(HookFunction, "func_name"):
             name = str( HookFunction.func_name )
-        elif hasattr(HookFunction,"__name__"):
+        elif hasattr(HookFunction, "__name__"):
             name = str( HookFunction.__name__ )
-        elif isinstance(HookFunction,str):
+        elif isinstance(HookFunction, str):
             name = str( HookFunction )
         else:
             name = None
         #
-        i = -1
+        ih = -1
         index_to_remove = []
-        for [hf, hp, hs] in self.__dataobservers:
-            i = i + 1
-            if name is hf.__name__ or AllObservers: index_to_remove.append( i )
+        for [hf, _, _, _, _, _] in self.__dataobservers:
+            ih = ih + 1
+            if name is hf.__name__ or AllObservers:
+                index_to_remove.append( ih )
         index_to_remove.reverse()
-        for i in index_to_remove:
-            self.__dataobservers.pop( i )
+        for ih in index_to_remove:
+            self.__dataobservers.pop( ih )
         return len(index_to_remove)
 
     def hasDataObserver(self):
@@ -671,14 +851,15 @@ class SchedulerTrigger(object):
     """
     Classe générale d'interface de type Scheduler/Trigger
     """
+    __slots__ = ()
+
     def __init__(self,
                  simplifiedCombo = None,
                  startTime       = 0,
                  endTime         = int( 1e9 ),
                  timeDelay       = 1,
                  timeUnit        = 1,
-                 frequency       = None,
-                ):
+                 frequency       = None ):
         pass
 
 # ==============================================================================
@@ -692,6 +873,8 @@ class OneScalar(Persistence):
     ou des matrices comme dans les classes suivantes, mais c'est déconseillé
     pour conserver une signification claire des noms.
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = float):
         Persistence.__init__(self, name, unit, basetype)
 
@@ -699,6 +882,8 @@ class OneIndex(Persistence):
     """
     Classe définissant le stockage d'une valeur unique entière (int) par pas.
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = int):
         Persistence.__init__(self, name, unit, basetype)
 
@@ -707,22 +892,37 @@ class OneVector(Persistence):
     Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
     pas utiliser cette classe pour des données hétérogènes, mais "OneList".
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = numpy.ravel):
         Persistence.__init__(self, name, unit, basetype)
 
+class OneMatrice(Persistence):
+    """
+    Classe de stockage d'une matrice de valeurs homogènes par pas.
+    """
+    __slots__ = ()
+
+    def __init__(self, name="", unit="", basetype = numpy.array):
+        Persistence.__init__(self, name, unit, basetype)
+
 class OneMatrix(Persistence):
     """
-    Classe de stockage d'une matrice de valeurs (numpy.matrix) par pas.
+    Classe de stockage d'une matrice de valeurs homogènes par pas.
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = numpy.matrix):
         Persistence.__init__(self, name, unit, basetype)
 
 class OneList(Persistence):
     """
-    Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne pas
-    utiliser cette classe pour des données numériques homogènes, mais
+    Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne
+    pas utiliser cette classe pour des données numériques homogènes, mais
     "OneVector".
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = list):
         Persistence.__init__(self, name, unit, basetype)
 
@@ -734,10 +934,12 @@ class OneNoType(Persistence):
     """
     Classe de stockage d'un objet sans modification (cast) de type. Attention,
     selon le véritable type de l'objet stocké à chaque pas, les opérations
-    arithmétiques à base de numpy peuvent être invalides ou donner des résultats
-    inattendus. Cette classe n'est donc à utiliser qu'à bon escient
+    arithmétiques à base de numpy peuvent être invalides ou donner des
+    résultats inattendus. Cette classe n'est donc à utiliser qu'à bon escient
     volontairement, et pas du tout par défaut.
     """
+    __slots__ = ()
+
     def __init__(self, name="", unit="", basetype = NoType):
         Persistence.__init__(self, name, unit, basetype)
 
@@ -750,13 +952,15 @@ class CompositePersistence(object):
     Des objets par défaut sont prévus, et des objets supplémentaires peuvent
     être ajoutés.
     """
+    __slots__ = ("__name", "__StoredObjects")
+
     def __init__(self, name="", defaults=True):
         """
         name : nom courant
 
-        La gestion interne des données est exclusivement basée sur les variables
-        initialisées ici (qui ne sont pas accessibles depuis l'extérieur des
-        objets comme des attributs) :
+        La gestion interne des données est exclusivement basée sur les
+        variables initialisées ici (qui ne sont pas accessibles depuis
+        l'extérieur des objets comme des attributs) :
         __StoredObjects : objets de type persistence collectés dans cet objet
         """
         self.__name = str(name)
@@ -785,17 +989,19 @@ class CompositePersistence(object):
         """
         Stockage d'une valeur "value" pour le "step" dans la variable "name".
         """
-        if name is None: raise ValueError("Storable object name is required for storage.")
+        if name is None:
+            raise ValueError("Storable object name is required for storage.")
         if name not in self.__StoredObjects.keys():
             raise ValueError("No such name '%s' exists in storable objects."%name)
         self.__StoredObjects[name].store( value=value, **kwargs )
 
     def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
         """
-        Ajoute dans les objets stockables un nouvel objet défini par son nom, son
-        type de Persistence et son type de base à chaque pas.
+        Ajoute dans les objets stockables un nouvel objet défini par son nom,
+        son type de Persistence et son type de base à chaque pas.
         """
-        if name is None: raise ValueError("Object name is required for adding an object.")
+        if name is None:
+            raise ValueError("Object name is required for adding an object.")
         if name in self.__StoredObjects.keys():
             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
         if basetype is None:
@@ -807,7 +1013,8 @@ class CompositePersistence(object):
         """
         Renvoie l'objet de type Persistence qui porte le nom demandé.
         """
-        if name is None: raise ValueError("Object name is required for retrieving an object.")
+        if name is None:
+            raise ValueError("Object name is required for retrieving an object.")
         if name not in self.__StoredObjects.keys():
             raise ValueError("No such name '%s' exists in stored objects."%name)
         return self.__StoredObjects[name]
@@ -819,7 +1026,8 @@ class CompositePersistence(object):
         comporter les méthodes habituelles de Persistence pour que cela
         fonctionne.
         """
-        if name is None: raise ValueError("Object name is required for setting an object.")
+        if name is None:
+            raise ValueError("Object name is required for setting an object.")
         if name in self.__StoredObjects.keys():
             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
         self.__StoredObjects[name] = objet
@@ -828,7 +1036,8 @@ class CompositePersistence(object):
         """
         Supprime un objet de la liste des objets stockables.
         """
-        if name is None: raise ValueError("Object name is required for retrieving an object.")
+        if name is None:
+            raise ValueError("Object name is required for retrieving an object.")
         if name not in self.__StoredObjects.keys():
             raise ValueError("No such name '%s' exists in stored objects."%name)
         del self.__StoredObjects[name]
@@ -863,7 +1072,8 @@ class CompositePersistence(object):
             usedObjs = []
             for k in objs:
                 try:
-                    if len(self.__StoredObjects[k]) > 0: usedObjs.append( k )
+                    if len(self.__StoredObjects[k]) > 0:
+                        usedObjs.append( k )
                 finally:
                     pass
             objs = usedObjs
@@ -926,4 +1136,4 @@ class CompositePersistence(object):
 
 # ==============================================================================
 if __name__ == "__main__":
-    print('\n AUTODIAGNOSTIC \n')
+    print("\n AUTODIAGNOSTIC\n")