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