]> SALOME platform Git repositories - modules/adao.git/blob - src/daComposant/daCore/Persistence.py
Salome HOME
Documentation, style and code performance update
[modules/adao.git] / src / daComposant / daCore / Persistence.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2008-2024 EDF R&D
4 #
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation; either
8 # version 2.1 of the License.
9 #
10 # This library is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this library; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18 #
19 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
20 #
21 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
22
23 """
24     Définit des outils de persistance et d'enregistrement de séries de valeurs
25     pour analyse ultérieure ou utilisation de calcul.
26 """
27 __author__ = "Jean-Philippe ARGAUD"
28 __all__ = []
29
30 import os, numpy, copy, math
31 import gzip, bz2, pickle
32
33 from daCore.PlatformInfo import PathManagement ; PathManagement()  # noqa: E702,E203
34 from daCore.PlatformInfo import has_gnuplot, PlatformInfo
35 mfp = PlatformInfo().MaximumPrecision()
36 if has_gnuplot:
37     import Gnuplot
38
39 # ==============================================================================
40 class Persistence(object):
41     """
42     Classe générale de persistance définissant les accesseurs nécessaires
43     (Template)
44     """
45     __slots__ = (
46         "__name", "__unit", "__basetype", "__values", "__tags", "__dynamic",
47         "__g", "__title", "__ltitle", "__pause", "__dataobservers",
48     )
49
50     def __init__(self, name="", unit="", basetype=str):
51         """
52         name : nom courant
53         unit : unité
54         basetype : type de base de l'objet stocké à chaque pas
55
56         La gestion interne des données est exclusivement basée sur les variables
57         initialisées ici (qui ne sont pas accessibles depuis l'extérieur des
58         objets comme des attributs) :
59         __basetype : le type de base de chaque valeur, sous la forme d'un type
60                      permettant l'instanciation ou le casting Python
61         __values : les valeurs de stockage. Par défaut, c'est None
62         """
63         self.__name = str(name)
64         self.__unit = str(unit)
65         #
66         self.__basetype = basetype
67         #
68         self.__values   = []
69         self.__tags     = []
70         #
71         self.__dynamic  = False
72         self.__g        = None
73         self.__title    = None
74         self.__ltitle   = None
75         self.__pause    = None
76         #
77         self.__dataobservers = []
78
79     def basetype(self, basetype=None):
80         """
81         Renvoie ou met en place le type de base des objets stockés
82         """
83         if basetype is None:
84             return self.__basetype
85         else:
86             self.__basetype = basetype
87
88     def store(self, value=None, **kwargs):
89         """
90         Stocke une valeur avec ses informations de filtrage.
91         """
92         if value is None:
93             raise ValueError("Value argument required")
94         #
95         self.__values.append(copy.copy(self.__basetype(value)))
96         self.__tags.append(kwargs)
97         #
98         if self.__dynamic:
99             self.__replots()
100         __step = len(self.__values) - 1
101         for hook, parameters, scheduler in self.__dataobservers:
102             if __step in scheduler:
103                 hook( self, parameters )
104
105     def pop(self, item=None):
106         """
107         Retire une valeur enregistrée par son index de stockage. Sans argument,
108         retire le dernier objet enregistre.
109         """
110         if item is not None:
111             __index = int(item)
112             self.__values.pop(__index)
113             self.__tags.pop(__index)
114         else:
115             self.__values.pop()
116             self.__tags.pop()
117
118     def shape(self):
119         """
120         Renvoie la taille sous forme numpy du dernier objet stocké. Si c'est un
121         objet numpy, renvoie le shape. Si c'est un entier, un flottant, un
122         complexe, renvoie 1. Si c'est une liste ou un dictionnaire, renvoie la
123         longueur. Par défaut, renvoie 1.
124         """
125         if len(self.__values) > 0:
126             if self.__basetype in [numpy.matrix, numpy.ndarray, numpy.array, numpy.ravel]:
127                 return self.__values[-1].shape
128             elif self.__basetype in [int, float]:
129                 return (1,)
130             elif self.__basetype in [list, dict]:
131                 return (len(self.__values[-1]),)
132             else:
133                 return (1,)
134         else:
135             raise ValueError("Object has no shape before its first storage")
136
137     # ---------------------------------------------------------
138     def __str__(self):
139         "x.__str__() <==> str(x)"
140         msg  = "   Index        Value   Tags\n"
141         for iv, vv in enumerate(self.__values):
142             msg += "  i=%05i  %10s   %s\n"%(iv, vv, self.__tags[iv])
143         return msg
144
145     def __len__(self):
146         "x.__len__() <==> len(x)"
147         return len(self.__values)
148
149     def name(self):
150         return self.__name
151
152     def __getitem__(self, index=None ):
153         "x.__getitem__(y) <==> x[y]"
154         return copy.copy(self.__values[index])
155
156     def count(self, value):
157         "L.count(value) -> integer -- return number of occurrences of value"
158         return self.__values.count(value)
159
160     def index(self, value, start=0, stop=None):
161         "L.index(value, [start, [stop]]) -> integer -- return first index of value."
162         if stop is None:
163             stop = len(self.__values)
164         return self.__values.index(value, start, stop)
165
166     # ---------------------------------------------------------
167     def __filteredIndexes(self, **kwargs):
168         "Function interne filtrant les index"
169         __indexOfFilteredItems = range(len(self.__tags))
170         __filteringKwTags = kwargs.keys()
171         if len(__filteringKwTags) > 0:
172             for tagKey in __filteringKwTags:
173                 __tmp = []
174                 for i in __indexOfFilteredItems:
175                     if tagKey in self.__tags[i]:
176                         if self.__tags[i][tagKey] == kwargs[tagKey]:
177                             __tmp.append( i )
178                         elif isinstance(kwargs[tagKey], (list, tuple)) and self.__tags[i][tagKey] in kwargs[tagKey]:
179                             __tmp.append( i )
180                 __indexOfFilteredItems = __tmp
181                 if len(__indexOfFilteredItems) == 0:
182                     break
183         return __indexOfFilteredItems
184
185     # ---------------------------------------------------------
186     def values(self, **kwargs):
187         "D.values() -> list of D's values"
188         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
189         return [self.__values[i] for i in __indexOfFilteredItems]
190
191     def keys(self, keyword=None, **kwargs):
192         "D.keys() -> list of D's keys"
193         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
194         __keys = []
195         for i in __indexOfFilteredItems:
196             if keyword in self.__tags[i]:
197                 __keys.append( self.__tags[i][keyword] )
198             else:
199                 __keys.append( None )
200         return __keys
201
202     def items(self, keyword=None, **kwargs):
203         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
204         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
205         __pairs = []
206         for i in __indexOfFilteredItems:
207             if keyword in self.__tags[i]:
208                 __pairs.append( (self.__tags[i][keyword], self.__values[i]) )
209             else:
210                 __pairs.append( (None, self.__values[i]) )
211         return __pairs
212
213     def tagkeys(self):
214         "D.tagkeys() -> list of D's tag keys"
215         __allKeys = []
216         for dicotags in self.__tags:
217             __allKeys.extend( list(dicotags.keys()) )
218         __allKeys = sorted(set(__allKeys))
219         return __allKeys
220
221     # def valueserie(self, item=None, allSteps=True, **kwargs):
222     #     if item is not None:
223     #         return self.__values[item]
224     #     else:
225     #         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
226     #         if not allSteps and len(__indexOfFilteredItems) > 0:
227     #             return self.__values[__indexOfFilteredItems[0]]
228     #         else:
229     #             return [self.__values[i] for i in __indexOfFilteredItems]
230
231     def tagserie(self, item=None, withValues=False, outputTag=None, **kwargs):
232         "D.tagserie() -> list of D's tag serie"
233         if item is None:
234             __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
235         else:
236             __indexOfFilteredItems = [item,]
237         #
238         # Dans le cas où la sortie donne les valeurs d'un "outputTag"
239         if outputTag is not None and isinstance(outputTag, str):
240             outputValues = []
241             for index in __indexOfFilteredItems:
242                 if outputTag in self.__tags[index].keys():
243                     outputValues.append( self.__tags[index][outputTag] )
244             outputValues = sorted(set(outputValues))
245             return outputValues
246         #
247         # Dans le cas où la sortie donne les tags satisfaisants aux conditions
248         else:
249             if withValues:
250                 return [self.__tags[index] for index in __indexOfFilteredItems]
251             else:
252                 allTags = {}
253                 for index in __indexOfFilteredItems:
254                     allTags.update( self.__tags[index] )
255                 allKeys = sorted(allTags.keys())
256                 return allKeys
257
258     # ---------------------------------------------------------
259     # Pour compatibilité
260     def stepnumber(self):
261         "Nombre de pas"
262         return len(self.__values)
263
264     # Pour compatibilité
265     def stepserie(self, **kwargs):
266         "Nombre de pas filtrés"
267         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
268         return __indexOfFilteredItems
269
270     # Pour compatibilité
271     def steplist(self, **kwargs):
272         "Nombre de pas filtrés"
273         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
274         return list(__indexOfFilteredItems)
275
276     # ---------------------------------------------------------
277     def means(self):
278         """
279         Renvoie la série contenant, à chaque pas, la valeur moyenne des données
280         au pas. Il faut que le type de base soit compatible avec les types
281         élémentaires numpy.
282         """
283         try:
284             return [numpy.mean(item, dtype=mfp).astype('float') for item in self.__values]
285         except Exception:
286             raise TypeError("Base type is incompatible with numpy")
287
288     def stds(self, ddof=0):
289         """
290         Renvoie la série contenant, à chaque pas, l'écart-type des données
291         au pas. Il faut que le type de base soit compatible avec les types
292         élémentaires numpy.
293
294         ddof : c'est le nombre de degrés de liberté pour le calcul de
295                l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
296         """
297         try:
298             if numpy.version.version >= '1.1.0':
299                 return [numpy.array(item).std(ddof=ddof, dtype=mfp).astype('float') for item in self.__values]
300             else:
301                 return [numpy.array(item).std(dtype=mfp).astype('float') for item in self.__values]
302         except Exception:
303             raise TypeError("Base type is incompatible with numpy")
304
305     def sums(self):
306         """
307         Renvoie la série contenant, à chaque pas, la somme des données au pas.
308         Il faut que le type de base soit compatible avec les types élémentaires
309         numpy.
310         """
311         try:
312             return [numpy.array(item).sum() for item in self.__values]
313         except Exception:
314             raise TypeError("Base type is incompatible with numpy")
315
316     def mins(self):
317         """
318         Renvoie la série contenant, à chaque pas, le minimum des données au pas.
319         Il faut que le type de base soit compatible avec les types élémentaires
320         numpy.
321         """
322         try:
323             return [numpy.array(item).min() for item in self.__values]
324         except Exception:
325             raise TypeError("Base type is incompatible with numpy")
326
327     def maxs(self):
328         """
329         Renvoie la série contenant, à chaque pas, la maximum des données au pas.
330         Il faut que le type de base soit compatible avec les types élémentaires
331         numpy.
332         """
333         try:
334             return [numpy.array(item).max() for item in self.__values]
335         except Exception:
336             raise TypeError("Base type is incompatible with numpy")
337
338     def powers(self, x2):
339         """
340         Renvoie la série contenant, à chaque pas, la puissance "**x2" au pas.
341         Il faut que le type de base soit compatible avec les types élémentaires
342         numpy.
343         """
344         try:
345             return [numpy.power(item, x2) for item in self.__values]
346         except Exception:
347             raise TypeError("Base type is incompatible with numpy")
348
349     def norms(self, _ord=None):
350         """
351         Norm (_ord : voir numpy.linalg.norm)
352
353         Renvoie la série contenant, à chaque pas, la norme des données au pas.
354         Il faut que le type de base soit compatible avec les types élémentaires
355         numpy.
356         """
357         try:
358             return [numpy.linalg.norm(item, _ord) for item in self.__values]
359         except Exception:
360             raise TypeError("Base type is incompatible with numpy")
361
362     def traces(self, offset=0):
363         """
364         Trace
365
366         Renvoie la série contenant, à chaque pas, la trace (avec l'offset) des
367         données au pas. Il faut que le type de base soit compatible avec les
368         types élémentaires numpy.
369         """
370         try:
371             return [numpy.trace(item, offset, dtype=mfp) for item in self.__values]
372         except Exception:
373             raise TypeError("Base type is incompatible with numpy")
374
375     def maes(self, _predictor=None):
376         """
377         Mean Absolute Error (MAE)
378         mae(dX) = 1/n sum(dX_i)
379
380         Renvoie la série contenant, à chaque pas, la MAE des données au pas.
381         Il faut que le type de base soit compatible avec les types élémentaires
382         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
383         prédicteur est None, sinon c'est appliqué à l'écart entre les données
384         au pas et le prédicteur au même pas.
385         """
386         if _predictor is None:
387             try:
388                 return [numpy.mean(numpy.abs(item)) for item in self.__values]
389             except Exception:
390                 raise TypeError("Base type is incompatible with numpy")
391         else:
392             if len(_predictor) != len(self.__values):
393                 raise ValueError("Predictor number of steps is incompatible with the values")
394             for i, item in enumerate(self.__values):
395                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
396                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
397             try:
398                 return [numpy.mean(numpy.abs(numpy.ravel(item) - numpy.ravel(_predictor[i]))) for i, item in enumerate(self.__values)]
399             except Exception:
400                 raise TypeError("Base type is incompatible with numpy")
401
402     def mses(self, _predictor=None):
403         """
404         Mean-Square Error (MSE) ou Mean-Square Deviation (MSD)
405         mse(dX) = 1/n sum(dX_i**2)
406
407         Renvoie la série contenant, à chaque pas, la MSE des données au pas. Il
408         faut que le type de base soit compatible avec les types élémentaires
409         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
410         prédicteur est None, sinon c'est appliqué à l'écart entre les données
411         au pas et le prédicteur au même pas.
412         """
413         if _predictor is None:
414             try:
415                 __n = self.shape()[0]
416                 return [(numpy.linalg.norm(item)**2 / __n) for item in self.__values]
417             except Exception:
418                 raise TypeError("Base type is incompatible with numpy")
419         else:
420             if len(_predictor) != len(self.__values):
421                 raise ValueError("Predictor number of steps is incompatible with the values")
422             for i, item in enumerate(self.__values):
423                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
424                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
425             try:
426                 __n = self.shape()[0]
427                 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i]))**2 / __n) for i, item in enumerate(self.__values)]
428             except Exception:
429                 raise TypeError("Base type is incompatible with numpy")
430
431     msds = mses  # Mean-Square Deviation (MSD=MSE)
432
433     def rmses(self, _predictor=None):
434         """
435         Root-Mean-Square Error (RMSE) ou Root-Mean-Square Deviation (RMSD)
436         rmse(dX) = sqrt( 1/n sum(dX_i**2) ) = sqrt( mse(dX) )
437
438         Renvoie la série contenant, à chaque pas, la RMSE des données au pas.
439         Il faut que le type de base soit compatible avec les types élémentaires
440         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
441         prédicteur est None, sinon c'est appliqué à l'écart entre les données
442         au pas et le prédicteur au même pas.
443         """
444         if _predictor is None:
445             try:
446                 __n = self.shape()[0]
447                 return [(numpy.linalg.norm(item) / math.sqrt(__n)) for item in self.__values]
448             except Exception:
449                 raise TypeError("Base type is incompatible with numpy")
450         else:
451             if len(_predictor) != len(self.__values):
452                 raise ValueError("Predictor number of steps is incompatible with the values")
453             for i, item in enumerate(self.__values):
454                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
455                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
456             try:
457                 __n = self.shape()[0]
458                 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i])) / math.sqrt(__n)) for i, item in enumerate(self.__values)]
459             except Exception:
460                 raise TypeError("Base type is incompatible with numpy")
461
462     rmsds = rmses  # Root-Mean-Square Deviation (RMSD=RMSE)
463
464     def __preplots(self,
465                    title    = "",
466                    xlabel   = "",
467                    ylabel   = "",
468                    ltitle   = None,
469                    geometry = "600x400",
470                    persist  = False,
471                    pause    = True ):
472         "Préparation des plots"
473         #
474         # Vérification de la disponibilité du module Gnuplot
475         if not has_gnuplot:
476             raise ImportError("The Gnuplot module is required to plot the object.")
477         #
478         # Vérification et compléments sur les paramètres d'entrée
479         if ltitle is None:
480             ltitle = ""
481         __geometry = str(geometry)
482         __sizespec = (__geometry.split('+')[0]).replace('x', ',')
483         #
484         if persist:
485             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
486         #
487         self.__g = Gnuplot.Gnuplot()  # persist=1
488         self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
489         self.__g('set style data lines')
490         self.__g('set grid')
491         self.__g('set autoscale')
492         self.__g('set xlabel "' + str(xlabel) + '"')
493         self.__g('set ylabel "' + str(ylabel) + '"')
494         self.__title  = title
495         self.__ltitle = ltitle
496         self.__pause  = pause
497
498     def plots(self,
499               item     = None,
500               step     = None,
501               steps    = None,
502               title    = "",
503               xlabel   = "",
504               ylabel   = "",
505               ltitle   = None,
506               geometry = "600x400",
507               filename = "",
508               dynamic  = False,
509               persist  = False,
510               pause    = True ):
511         """
512         Renvoie un affichage de la valeur à chaque pas, si elle est compatible
513         avec un affichage Gnuplot (donc essentiellement un vecteur). Si
514         l'argument "step" existe dans la liste des pas de stockage effectués,
515         renvoie l'affichage de la valeur stockée à ce pas "step". Si l'argument
516         "item" est correct, renvoie l'affichage de la valeur stockée au numéro
517         "item". Par défaut ou en l'absence de "step" ou "item", renvoie un
518         affichage successif de tous les pas.
519
520         Arguments :
521             - step     : valeur du pas à afficher
522             - item     : index de la valeur à afficher
523             - steps    : liste unique des pas de l'axe des X, ou None si c'est
524                          la numérotation par défaut
525             - title    : base du titre général, qui sera automatiquement
526                          complétée par la mention du pas
527             - xlabel   : label de l'axe des X
528             - ylabel   : label de l'axe des Y
529             - ltitle   : titre associé au vecteur tracé
530             - geometry : taille en pixels de la fenêtre et position du coin haut
531                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
532             - filename : base de nom de fichier Postscript pour une sauvegarde,
533                          qui est automatiquement complétée par le numéro du
534                          fichier calculé par incrément simple de compteur
535             - dynamic  : effectue un affichage des valeurs à chaque stockage
536                          (au-delà du second). La méthode "plots" permet de
537                          déclarer l'affichage dynamique, et c'est la méthode
538                          "__replots" qui est utilisée pour l'effectuer
539             - persist  : booléen indiquant que la fenêtre affichée sera
540                          conservée lors du passage au dessin suivant
541                          Par défaut, persist = False
542             - pause    : booléen indiquant une pause après chaque tracé, et
543                          attendant un Return
544                          Par défaut, pause = True
545         """
546         if not self.__dynamic:
547             self.__preplots(title, xlabel, ylabel, ltitle, geometry, persist, pause )
548             if dynamic:
549                 self.__dynamic = True
550                 if len(self.__values) == 0:
551                     return 0
552         #
553         # Tracé du ou des vecteurs demandés
554         indexes = []
555         if step is not None and step < len(self.__values):
556             indexes.append(step)
557         elif item is not None and item < len(self.__values):
558             indexes.append(item)
559         else:
560             indexes = indexes + list(range(len(self.__values)))
561         #
562         i = -1
563         for index in indexes:
564             self.__g('set title  "' + str(title) + ' (pas ' + str(index) + ')"')
565             if isinstance(steps, (list, numpy.ndarray)):
566                 Steps = list(steps)
567             else:
568                 Steps = list(range(len(self.__values[index])))
569             #
570             self.__g.plot( Gnuplot.Data( Steps, self.__values[index], title=ltitle ) )
571             #
572             if filename != "":
573                 i += 1
574                 stepfilename = "%s_%03i.ps"%(filename, i)
575                 if os.path.isfile(stepfilename):
576                     raise ValueError("Error: a file with this name \"%s\" already exists."%stepfilename)
577                 self.__g.hardcopy(filename=stepfilename, color=1)
578             if self.__pause:
579                 eval(input('Please press return to continue...\n'))
580
581     def __replots(self):
582         """
583         Affichage dans le cas du suivi dynamique de la variable
584         """
585         if self.__dynamic and len(self.__values) < 2:
586             return 0
587         #
588         self.__g('set title  "' + str(self.__title))
589         Steps = list(range(len(self.__values)))
590         self.__g.plot( Gnuplot.Data( Steps, self.__values, title=self.__ltitle ) )
591         #
592         if self.__pause:
593             eval(input('Please press return to continue...\n'))
594
595     # ---------------------------------------------------------
596     # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
597     def mean(self):
598         """
599         Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
600         longueur des pas. Il faut que le type de base soit compatible avec
601         les types élémentaires numpy.
602         """
603         try:
604             return numpy.mean(self.__values, axis=0, dtype=mfp).astype('float')
605         except Exception:
606             raise TypeError("Base type is incompatible with numpy")
607
608     def std(self, ddof=0):
609         """
610         Renvoie l'écart-type de toutes les valeurs sans tenir compte de la
611         longueur des pas. Il faut que le type de base soit compatible avec
612         les types élémentaires numpy.
613
614         ddof : c'est le nombre de degrés de liberté pour le calcul de
615                l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
616         """
617         try:
618             if numpy.version.version >= '1.1.0':
619                 return numpy.asarray(self.__values).std(ddof=ddof, axis=0).astype('float')
620             else:
621                 return numpy.asarray(self.__values).std(axis=0).astype('float')
622         except Exception:
623             raise TypeError("Base type is incompatible with numpy")
624
625     def sum(self):
626         """
627         Renvoie la somme de toutes les valeurs sans tenir compte de la
628         longueur des pas. Il faut que le type de base soit compatible avec
629         les types élémentaires numpy.
630         """
631         try:
632             return numpy.asarray(self.__values).sum(axis=0)
633         except Exception:
634             raise TypeError("Base type is incompatible with numpy")
635
636     def min(self):
637         """
638         Renvoie le minimum de toutes les valeurs sans tenir compte de la
639         longueur des pas. Il faut que le type de base soit compatible avec
640         les types élémentaires numpy.
641         """
642         try:
643             return numpy.asarray(self.__values).min(axis=0)
644         except Exception:
645             raise TypeError("Base type is incompatible with numpy")
646
647     def max(self):
648         """
649         Renvoie le maximum de toutes les valeurs sans tenir compte de la
650         longueur des pas. Il faut que le type de base soit compatible avec
651         les types élémentaires numpy.
652         """
653         try:
654             return numpy.asarray(self.__values).max(axis=0)
655         except Exception:
656             raise TypeError("Base type is incompatible with numpy")
657
658     def cumsum(self):
659         """
660         Renvoie la somme cumulée de toutes les valeurs sans tenir compte de la
661         longueur des pas. Il faut que le type de base soit compatible avec
662         les types élémentaires numpy.
663         """
664         try:
665             return numpy.asarray(self.__values).cumsum(axis=0)
666         except Exception:
667             raise TypeError("Base type is incompatible with numpy")
668
669     def plot(self,
670              steps    = None,
671              title    = "",
672              xlabel   = "",
673              ylabel   = "",
674              ltitle   = None,
675              geometry = "600x400",
676              filename = "",
677              persist  = False,
678              pause    = True ):
679         """
680         Renvoie un affichage unique pour l'ensemble des valeurs à chaque pas, si
681         elles sont compatibles avec un affichage Gnuplot (donc essentiellement
682         un vecteur). Si l'argument "step" existe dans la liste des pas de
683         stockage effectués, renvoie l'affichage de la valeur stockée à ce pas
684         "step". Si l'argument "item" est correct, renvoie l'affichage de la
685         valeur stockée au numéro "item".
686
687         Arguments :
688             - steps    : liste unique des pas de l'axe des X, ou None si c'est
689                          la numérotation par défaut
690             - title    : base du titre général, qui sera automatiquement
691                          complétée par la mention du pas
692             - xlabel   : label de l'axe des X
693             - ylabel   : label de l'axe des Y
694             - ltitle   : titre associé au vecteur tracé
695             - geometry : taille en pixels de la fenêtre et position du coin haut
696                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
697             - filename : nom de fichier Postscript pour une sauvegarde
698             - persist  : booléen indiquant que la fenêtre affichée sera
699                          conservée lors du passage au dessin suivant
700                          Par défaut, persist = False
701             - pause    : booléen indiquant une pause après chaque tracé, et
702                          attendant un Return
703                          Par défaut, pause = True
704         """
705         #
706         # Vérification de la disponibilité du module Gnuplot
707         if not has_gnuplot:
708             raise ImportError("The Gnuplot module is required to plot the object.")
709         #
710         # Vérification et compléments sur les paramètres d'entrée
711         if ltitle is None:
712             ltitle = ""
713         if isinstance(steps, (list, numpy.ndarray)):
714             Steps = list(steps)
715         else:
716             Steps = list(range(len(self.__values[0])))
717         __geometry = str(geometry)
718         __sizespec = (__geometry.split('+')[0]).replace('x', ',')
719         #
720         if persist:
721             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
722         #
723         self.__g = Gnuplot.Gnuplot()  # persist=1
724         self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
725         self.__g('set style data lines')
726         self.__g('set grid')
727         self.__g('set autoscale')
728         self.__g('set title  "' + str(title)  + '"')
729         self.__g('set xlabel "' + str(xlabel) + '"')
730         self.__g('set ylabel "' + str(ylabel) + '"')
731         #
732         # Tracé du ou des vecteurs demandés
733         indexes = list(range(len(self.__values)))
734         self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle + " (pas 0)" ) )
735         for index in indexes:
736             self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle + " (pas %i)"%index ) )
737         #
738         if filename != "":
739             self.__g.hardcopy(filename=filename, color=1)
740         if pause:
741             eval(input('Please press return to continue...\n'))
742
743     # ---------------------------------------------------------
744     def s2mvr(self):
745         """
746         Renvoie la série sous la forme d'une unique matrice avec les données au
747         pas rangées par ligne
748         """
749         try:
750             return numpy.asarray(self.__values)
751         except Exception:
752             raise TypeError("Base type is incompatible with numpy")
753
754     def s2mvc(self):
755         """
756         Renvoie la série sous la forme d'une unique matrice avec les données au
757         pas rangées par colonne
758         """
759         try:
760             return numpy.asarray(self.__values).transpose()
761             # Eqvlt: return numpy.stack([numpy.ravel(sv) for sv in self.__values], axis=1)
762         except Exception:
763             raise TypeError("Base type is incompatible with numpy")
764
765     # ---------------------------------------------------------
766     def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None):
767         """
768         Association à la variable d'un triplet définissant un observer
769
770         Le Scheduler attendu est une fréquence, une simple liste d'index ou un
771         range des index.
772         """
773         #
774         # Vérification du Scheduler
775         # -------------------------
776         maxiter = int( 1e9 )
777         if isinstance(Scheduler, int):                # Considéré comme une fréquence à partir de 0
778             Schedulers = range( 0, maxiter, int(Scheduler) )
779         elif isinstance(Scheduler, range):            # Considéré comme un itérateur
780             Schedulers = Scheduler
781         elif isinstance(Scheduler, (list, tuple)):    # Considéré comme des index explicites
782             Schedulers = [int(i) for i in Scheduler]  # Similaire à map( int, Scheduler )  # noqa: E262
783         else:                                         # Dans tous les autres cas, activé par défaut
784             Schedulers = range( 0, maxiter )
785         #
786         # Stockage interne de l'observer dans la variable
787         # -----------------------------------------------
788         self.__dataobservers.append( [HookFunction, HookParameters, Schedulers] )
789
790     def removeDataObserver(self, HookFunction = None, AllObservers = False):
791         """
792         Suppression d'un observer nommé sur la variable.
793
794         On peut donner dans HookFunction la meme fonction que lors de la
795         définition, ou un simple string qui est le nom de la fonction. Si
796         AllObservers est vrai, supprime tous les observers enregistrés.
797         """
798         if hasattr(HookFunction, "func_name"):
799             name = str( HookFunction.func_name )
800         elif hasattr(HookFunction, "__name__"):
801             name = str( HookFunction.__name__ )
802         elif isinstance(HookFunction, str):
803             name = str( HookFunction )
804         else:
805             name = None
806         #
807         ih = -1
808         index_to_remove = []
809         for [hf, hp, hs] in self.__dataobservers:
810             ih = ih + 1
811             if name is hf.__name__ or AllObservers:
812                 index_to_remove.append( ih )
813         index_to_remove.reverse()
814         for ih in index_to_remove:
815             self.__dataobservers.pop( ih )
816         return len(index_to_remove)
817
818     def hasDataObserver(self):
819         return bool(len(self.__dataobservers) > 0)
820
821 # ==============================================================================
822 class SchedulerTrigger(object):
823     """
824     Classe générale d'interface de type Scheduler/Trigger
825     """
826     __slots__ = ()
827
828     def __init__(self,
829                  simplifiedCombo = None,
830                  startTime       = 0,
831                  endTime         = int( 1e9 ),
832                  timeDelay       = 1,
833                  timeUnit        = 1,
834                  frequency       = None ):
835         pass
836
837 # ==============================================================================
838 class OneScalar(Persistence):
839     """
840     Classe définissant le stockage d'une valeur unique réelle (float) par pas.
841
842     Le type de base peut être changé par la méthode "basetype", mais il faut que
843     le nouveau type de base soit compatible avec les types par éléments de
844     numpy. On peut même utiliser cette classe pour stocker des vecteurs/listes
845     ou des matrices comme dans les classes suivantes, mais c'est déconseillé
846     pour conserver une signification claire des noms.
847     """
848     __slots__ = ()
849
850     def __init__(self, name="", unit="", basetype = float):
851         Persistence.__init__(self, name, unit, basetype)
852
853 class OneIndex(Persistence):
854     """
855     Classe définissant le stockage d'une valeur unique entière (int) par pas.
856     """
857     __slots__ = ()
858
859     def __init__(self, name="", unit="", basetype = int):
860         Persistence.__init__(self, name, unit, basetype)
861
862 class OneVector(Persistence):
863     """
864     Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
865     pas utiliser cette classe pour des données hétérogènes, mais "OneList".
866     """
867     __slots__ = ()
868
869     def __init__(self, name="", unit="", basetype = numpy.ravel):
870         Persistence.__init__(self, name, unit, basetype)
871
872 class OneMatrice(Persistence):
873     """
874     Classe de stockage d'une matrice de valeurs homogènes par pas.
875     """
876     __slots__ = ()
877
878     def __init__(self, name="", unit="", basetype = numpy.array):
879         Persistence.__init__(self, name, unit, basetype)
880
881 class OneMatrix(Persistence):
882     """
883     Classe de stockage d'une matrice de valeurs homogènes par pas.
884     """
885     __slots__ = ()
886
887     def __init__(self, name="", unit="", basetype = numpy.matrix):
888         Persistence.__init__(self, name, unit, basetype)
889
890 class OneList(Persistence):
891     """
892     Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne
893     pas utiliser cette classe pour des données numériques homogènes, mais
894     "OneVector".
895     """
896     __slots__ = ()
897
898     def __init__(self, name="", unit="", basetype = list):
899         Persistence.__init__(self, name, unit, basetype)
900
901 def NoType( value ):
902     "Fonction transparente, sans effet sur son argument"
903     return value
904
905 class OneNoType(Persistence):
906     """
907     Classe de stockage d'un objet sans modification (cast) de type. Attention,
908     selon le véritable type de l'objet stocké à chaque pas, les opérations
909     arithmétiques à base de numpy peuvent être invalides ou donner des
910     résultats inattendus. Cette classe n'est donc à utiliser qu'à bon escient
911     volontairement, et pas du tout par défaut.
912     """
913     __slots__ = ()
914
915     def __init__(self, name="", unit="", basetype = NoType):
916         Persistence.__init__(self, name, unit, basetype)
917
918 # ==============================================================================
919 class CompositePersistence(object):
920     """
921     Structure de stockage permettant de rassembler plusieurs objets de
922     persistence.
923
924     Des objets par défaut sont prévus, et des objets supplémentaires peuvent
925     être ajoutés.
926     """
927     __slots__ = ("__name", "__StoredObjects")
928
929     def __init__(self, name="", defaults=True):
930         """
931         name : nom courant
932
933         La gestion interne des données est exclusivement basée sur les
934         variables initialisées ici (qui ne sont pas accessibles depuis
935         l'extérieur des objets comme des attributs) :
936         __StoredObjects : objets de type persistence collectés dans cet objet
937         """
938         self.__name = str(name)
939         #
940         self.__StoredObjects = {}
941         #
942         # Definition des objets par defaut
943         # --------------------------------
944         if defaults:
945             self.__StoredObjects["Informations"]     = OneNoType("Informations")
946             self.__StoredObjects["Background"]       = OneVector("Background", basetype=numpy.array)
947             self.__StoredObjects["BackgroundError"]  = OneMatrix("BackgroundError")
948             self.__StoredObjects["Observation"]      = OneVector("Observation", basetype=numpy.array)
949             self.__StoredObjects["ObservationError"] = OneMatrix("ObservationError")
950             self.__StoredObjects["Analysis"]         = OneVector("Analysis", basetype=numpy.array)
951             self.__StoredObjects["AnalysisError"]    = OneMatrix("AnalysisError")
952             self.__StoredObjects["Innovation"]       = OneVector("Innovation", basetype=numpy.array)
953             self.__StoredObjects["KalmanGainK"]      = OneMatrix("KalmanGainK")
954             self.__StoredObjects["OperatorH"]        = OneMatrix("OperatorH")
955             self.__StoredObjects["RmsOMA"]           = OneScalar("RmsOMA")
956             self.__StoredObjects["RmsOMB"]           = OneScalar("RmsOMB")
957             self.__StoredObjects["RmsBMA"]           = OneScalar("RmsBMA")
958         #
959
960     def store(self, name=None, value=None, **kwargs):
961         """
962         Stockage d'une valeur "value" pour le "step" dans la variable "name".
963         """
964         if name is None:
965             raise ValueError("Storable object name is required for storage.")
966         if name not in self.__StoredObjects.keys():
967             raise ValueError("No such name '%s' exists in storable objects."%name)
968         self.__StoredObjects[name].store( value=value, **kwargs )
969
970     def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
971         """
972         Ajoute dans les objets stockables un nouvel objet défini par son nom,
973         son type de Persistence et son type de base à chaque pas.
974         """
975         if name is None:
976             raise ValueError("Object name is required for adding an object.")
977         if name in self.__StoredObjects.keys():
978             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
979         if basetype is None:
980             self.__StoredObjects[name] = persistenceType( name=str(name) )
981         else:
982             self.__StoredObjects[name] = persistenceType( name=str(name), basetype=basetype )
983
984     def get_object(self, name=None ):
985         """
986         Renvoie l'objet de type Persistence qui porte le nom demandé.
987         """
988         if name is None:
989             raise ValueError("Object name is required for retrieving an object.")
990         if name not in self.__StoredObjects.keys():
991             raise ValueError("No such name '%s' exists in stored objects."%name)
992         return self.__StoredObjects[name]
993
994     def set_object(self, name=None, objet=None ):
995         """
996         Affecte directement un 'objet' qui porte le nom 'name' demandé.
997         Attention, il n'est pas effectué de vérification sur le type, qui doit
998         comporter les méthodes habituelles de Persistence pour que cela
999         fonctionne.
1000         """
1001         if name is None:
1002             raise ValueError("Object name is required for setting an object.")
1003         if name in self.__StoredObjects.keys():
1004             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
1005         self.__StoredObjects[name] = objet
1006
1007     def del_object(self, name=None ):
1008         """
1009         Supprime un objet de la liste des objets stockables.
1010         """
1011         if name is None:
1012             raise ValueError("Object name is required for retrieving an object.")
1013         if name not in self.__StoredObjects.keys():
1014             raise ValueError("No such name '%s' exists in stored objects."%name)
1015         del self.__StoredObjects[name]
1016
1017     # ---------------------------------------------------------
1018     # Méthodes d'accès de type dictionnaire
1019     def __getitem__(self, name=None ):
1020         "x.__getitem__(y) <==> x[y]"
1021         return self.get_object( name )
1022
1023     def __setitem__(self, name=None, objet=None ):
1024         "x.__setitem__(i, y) <==> x[i]=y"
1025         self.set_object( name, objet )
1026
1027     def keys(self):
1028         "D.keys() -> list of D's keys"
1029         return self.get_stored_objects(hideVoidObjects = False)
1030
1031     def values(self):
1032         "D.values() -> list of D's values"
1033         return self.__StoredObjects.values()
1034
1035     def items(self):
1036         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
1037         return self.__StoredObjects.items()
1038
1039     # ---------------------------------------------------------
1040     def get_stored_objects(self, hideVoidObjects = False):
1041         "Renvoie la liste des objets présents"
1042         objs = self.__StoredObjects.keys()
1043         if hideVoidObjects:
1044             usedObjs = []
1045             for k in objs:
1046                 try:
1047                     if len(self.__StoredObjects[k]) > 0:
1048                         usedObjs.append( k )
1049                 finally:
1050                     pass
1051             objs = usedObjs
1052         objs = sorted(objs)
1053         return objs
1054
1055     # ---------------------------------------------------------
1056     def save_composite(self, filename=None, mode="pickle", compress="gzip"):
1057         """
1058         Enregistre l'objet dans le fichier indiqué selon le "mode" demandé,
1059         et renvoi le nom du fichier
1060         """
1061         if filename is None:
1062             if compress == "gzip":
1063                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.gz"
1064             elif compress == "bzip2":
1065                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.bz2"
1066             else:
1067                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl"
1068         else:
1069             filename = os.path.abspath( filename )
1070         #
1071         if mode == "pickle":
1072             if compress == "gzip":
1073                 output = gzip.open( filename, 'wb')
1074             elif compress == "bzip2":
1075                 output = bz2.BZ2File( filename, 'wb')
1076             else:
1077                 output = open( filename, 'wb')
1078             pickle.dump(self, output)
1079             output.close()
1080         else:
1081             raise ValueError("Save mode '%s' unknown. Choose another one."%mode)
1082         #
1083         return filename
1084
1085     def load_composite(self, filename=None, mode="pickle", compress="gzip"):
1086         """
1087         Recharge un objet composite sauvé en fichier
1088         """
1089         if filename is None:
1090             raise ValueError("A file name if requested to load a composite.")
1091         else:
1092             filename = os.path.abspath( filename )
1093         #
1094         if mode == "pickle":
1095             if compress == "gzip":
1096                 pkl_file = gzip.open( filename, 'rb')
1097             elif compress == "bzip2":
1098                 pkl_file = bz2.BZ2File( filename, 'rb')
1099             else:
1100                 pkl_file = open(filename, 'rb')
1101             output = pickle.load(pkl_file)
1102             for k in output.keys():
1103                 self[k] = output[k]
1104         else:
1105             raise ValueError("Load mode '%s' unknown. Choose another one."%mode)
1106         #
1107         return filename
1108
1109 # ==============================================================================
1110 if __name__ == "__main__":
1111     print("\n AUTODIAGNOSTIC\n")