Salome HOME
Code improvements, review and simplifications (2)
[modules/adao.git] / src / daComposant / daCore / Persistence.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2008-2022 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None):
709         """
710         Association à la variable d'un triplet définissant un observer
711
712         Le Scheduler attendu est une fréquence, une simple liste d'index ou un
713         range des index.
714         """
715         #
716         # Vérification du Scheduler
717         # -------------------------
718         maxiter = int( 1e9 )
719         if isinstance(Scheduler,int):      # Considéré comme une fréquence à partir de 0
720             Schedulers = range( 0, maxiter, int(Scheduler) )
721         elif isinstance(Scheduler,range):  # Considéré comme un itérateur
722             Schedulers = Scheduler
723         elif isinstance(Scheduler,(list,tuple)):   # Considéré comme des index explicites
724             Schedulers = [int(i) for i in Scheduler] # map( long, Scheduler )
725         else:                              # Dans tous les autres cas, activé par défaut
726             Schedulers = range( 0, maxiter )
727         #
728         # Stockage interne de l'observer dans la variable
729         # -----------------------------------------------
730         self.__dataobservers.append( [HookFunction, HookParameters, Schedulers] )
731
732     def removeDataObserver(self, HookFunction = None, AllObservers = False):
733         """
734         Suppression d'un observer nommé sur la variable.
735
736         On peut donner dans HookFunction la meme fonction que lors de la
737         définition, ou un simple string qui est le nom de la fonction. Si
738         AllObservers est vrai, supprime tous les observers enregistrés.
739         """
740         if hasattr(HookFunction,"func_name"):
741             name = str( HookFunction.func_name )
742         elif hasattr(HookFunction,"__name__"):
743             name = str( HookFunction.__name__ )
744         elif isinstance(HookFunction,str):
745             name = str( HookFunction )
746         else:
747             name = None
748         #
749         i = -1
750         index_to_remove = []
751         for [hf, hp, hs] in self.__dataobservers:
752             i = i + 1
753             if name is hf.__name__ or AllObservers: index_to_remove.append( i )
754         index_to_remove.reverse()
755         for i in index_to_remove:
756             self.__dataobservers.pop( i )
757         return len(index_to_remove)
758
759     def hasDataObserver(self):
760         return bool(len(self.__dataobservers) > 0)
761
762 # ==============================================================================
763 class SchedulerTrigger(object):
764     """
765     Classe générale d'interface de type Scheduler/Trigger
766     """
767     def __init__(self,
768                  simplifiedCombo = None,
769                  startTime       = 0,
770                  endTime         = int( 1e9 ),
771                  timeDelay       = 1,
772                  timeUnit        = 1,
773                  frequency       = None,
774                 ):
775         pass
776
777 # ==============================================================================
778 class OneScalar(Persistence):
779     """
780     Classe définissant le stockage d'une valeur unique réelle (float) par pas.
781
782     Le type de base peut être changé par la méthode "basetype", mais il faut que
783     le nouveau type de base soit compatible avec les types par éléments de
784     numpy. On peut même utiliser cette classe pour stocker des vecteurs/listes
785     ou des matrices comme dans les classes suivantes, mais c'est déconseillé
786     pour conserver une signification claire des noms.
787     """
788     def __init__(self, name="", unit="", basetype = float):
789         Persistence.__init__(self, name, unit, basetype)
790
791 class OneIndex(Persistence):
792     """
793     Classe définissant le stockage d'une valeur unique entière (int) par pas.
794     """
795     def __init__(self, name="", unit="", basetype = int):
796         Persistence.__init__(self, name, unit, basetype)
797
798 class OneVector(Persistence):
799     """
800     Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
801     pas utiliser cette classe pour des données hétérogènes, mais "OneList".
802     """
803     def __init__(self, name="", unit="", basetype = numpy.ravel):
804         Persistence.__init__(self, name, unit, basetype)
805
806 class OneMatrix(Persistence):
807     """
808     Classe de stockage d'une matrice de valeurs homogènes par pas.
809     """
810     def __init__(self, name="", unit="", basetype = numpy.matrix):
811         Persistence.__init__(self, name, unit, basetype)
812
813 class OneList(Persistence):
814     """
815     Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne
816     pas utiliser cette classe pour des données numériques homogènes, mais
817     "OneVector".
818     """
819     def __init__(self, name="", unit="", basetype = list):
820         Persistence.__init__(self, name, unit, basetype)
821
822 def NoType( value ):
823     "Fonction transparente, sans effet sur son argument"
824     return value
825
826 class OneNoType(Persistence):
827     """
828     Classe de stockage d'un objet sans modification (cast) de type. Attention,
829     selon le véritable type de l'objet stocké à chaque pas, les opérations
830     arithmétiques à base de numpy peuvent être invalides ou donner des
831     résultats inattendus. Cette classe n'est donc à utiliser qu'à bon escient
832     volontairement, et pas du tout par défaut.
833     """
834     def __init__(self, name="", unit="", basetype = NoType):
835         Persistence.__init__(self, name, unit, basetype)
836
837 # ==============================================================================
838 class CompositePersistence(object):
839     """
840     Structure de stockage permettant de rassembler plusieurs objets de
841     persistence.
842
843     Des objets par défaut sont prévus, et des objets supplémentaires peuvent
844     être ajoutés.
845     """
846     def __init__(self, name="", defaults=True):
847         """
848         name : nom courant
849
850         La gestion interne des données est exclusivement basée sur les
851         variables initialisées ici (qui ne sont pas accessibles depuis
852         l'extérieur des objets comme des attributs) :
853         __StoredObjects : objets de type persistence collectés dans cet objet
854         """
855         self.__name = str(name)
856         #
857         self.__StoredObjects = {}
858         #
859         # Definition des objets par defaut
860         # --------------------------------
861         if defaults:
862             self.__StoredObjects["Informations"]     = OneNoType("Informations")
863             self.__StoredObjects["Background"]       = OneVector("Background", basetype=numpy.array)
864             self.__StoredObjects["BackgroundError"]  = OneMatrix("BackgroundError")
865             self.__StoredObjects["Observation"]      = OneVector("Observation", basetype=numpy.array)
866             self.__StoredObjects["ObservationError"] = OneMatrix("ObservationError")
867             self.__StoredObjects["Analysis"]         = OneVector("Analysis", basetype=numpy.array)
868             self.__StoredObjects["AnalysisError"]    = OneMatrix("AnalysisError")
869             self.__StoredObjects["Innovation"]       = OneVector("Innovation", basetype=numpy.array)
870             self.__StoredObjects["KalmanGainK"]      = OneMatrix("KalmanGainK")
871             self.__StoredObjects["OperatorH"]        = OneMatrix("OperatorH")
872             self.__StoredObjects["RmsOMA"]           = OneScalar("RmsOMA")
873             self.__StoredObjects["RmsOMB"]           = OneScalar("RmsOMB")
874             self.__StoredObjects["RmsBMA"]           = OneScalar("RmsBMA")
875         #
876
877     def store(self, name=None, value=None, **kwargs):
878         """
879         Stockage d'une valeur "value" pour le "step" dans la variable "name".
880         """
881         if name is None: raise ValueError("Storable object name is required for storage.")
882         if name not in self.__StoredObjects.keys():
883             raise ValueError("No such name '%s' exists in storable objects."%name)
884         self.__StoredObjects[name].store( value=value, **kwargs )
885
886     def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
887         """
888         Ajoute dans les objets stockables un nouvel objet défini par son nom,
889         son type de Persistence et son type de base à chaque pas.
890         """
891         if name is None: raise ValueError("Object name is required for adding an object.")
892         if name in self.__StoredObjects.keys():
893             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
894         if basetype is None:
895             self.__StoredObjects[name] = persistenceType( name=str(name) )
896         else:
897             self.__StoredObjects[name] = persistenceType( name=str(name), basetype=basetype )
898
899     def get_object(self, name=None ):
900         """
901         Renvoie l'objet de type Persistence qui porte le nom demandé.
902         """
903         if name is None: raise ValueError("Object name is required for retrieving an object.")
904         if name not in self.__StoredObjects.keys():
905             raise ValueError("No such name '%s' exists in stored objects."%name)
906         return self.__StoredObjects[name]
907
908     def set_object(self, name=None, objet=None ):
909         """
910         Affecte directement un 'objet' qui porte le nom 'name' demandé.
911         Attention, il n'est pas effectué de vérification sur le type, qui doit
912         comporter les méthodes habituelles de Persistence pour que cela
913         fonctionne.
914         """
915         if name is None: raise ValueError("Object name is required for setting an object.")
916         if name in self.__StoredObjects.keys():
917             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
918         self.__StoredObjects[name] = objet
919
920     def del_object(self, name=None ):
921         """
922         Supprime un objet de la liste des objets stockables.
923         """
924         if name is None: raise ValueError("Object name is required for retrieving an object.")
925         if name not in self.__StoredObjects.keys():
926             raise ValueError("No such name '%s' exists in stored objects."%name)
927         del self.__StoredObjects[name]
928
929     # ---------------------------------------------------------
930     # Méthodes d'accès de type dictionnaire
931     def __getitem__(self, name=None ):
932         "x.__getitem__(y) <==> x[y]"
933         return self.get_object( name )
934
935     def __setitem__(self, name=None, objet=None ):
936         "x.__setitem__(i, y) <==> x[i]=y"
937         self.set_object( name, objet )
938
939     def keys(self):
940         "D.keys() -> list of D's keys"
941         return self.get_stored_objects(hideVoidObjects = False)
942
943     def values(self):
944         "D.values() -> list of D's values"
945         return self.__StoredObjects.values()
946
947     def items(self):
948         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
949         return self.__StoredObjects.items()
950
951     # ---------------------------------------------------------
952     def get_stored_objects(self, hideVoidObjects = False):
953         "Renvoie la liste des objets présents"
954         objs = self.__StoredObjects.keys()
955         if hideVoidObjects:
956             usedObjs = []
957             for k in objs:
958                 try:
959                     if len(self.__StoredObjects[k]) > 0: usedObjs.append( k )
960                 finally:
961                     pass
962             objs = usedObjs
963         objs = sorted(objs)
964         return objs
965
966     # ---------------------------------------------------------
967     def save_composite(self, filename=None, mode="pickle", compress="gzip"):
968         """
969         Enregistre l'objet dans le fichier indiqué selon le "mode" demandé,
970         et renvoi le nom du fichier
971         """
972         if filename is None:
973             if compress == "gzip":
974                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.gz"
975             elif compress == "bzip2":
976                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.bz2"
977             else:
978                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl"
979         else:
980             filename = os.path.abspath( filename )
981         #
982         if mode == "pickle":
983             if compress == "gzip":
984                 output = gzip.open( filename, 'wb')
985             elif compress == "bzip2":
986                 output = bz2.BZ2File( filename, 'wb')
987             else:
988                 output = open( filename, 'wb')
989             pickle.dump(self, output)
990             output.close()
991         else:
992             raise ValueError("Save mode '%s' unknown. Choose another one."%mode)
993         #
994         return filename
995
996     def load_composite(self, filename=None, mode="pickle", compress="gzip"):
997         """
998         Recharge un objet composite sauvé en fichier
999         """
1000         if filename is None:
1001             raise ValueError("A file name if requested to load a composite.")
1002         else:
1003             filename = os.path.abspath( filename )
1004         #
1005         if mode == "pickle":
1006             if compress == "gzip":
1007                 pkl_file = gzip.open( filename, 'rb')
1008             elif compress == "bzip2":
1009                 pkl_file = bz2.BZ2File( filename, 'rb')
1010             else:
1011                 pkl_file = open(filename, 'rb')
1012             output = pickle.load(pkl_file)
1013             for k in output.keys():
1014                 self[k] = output[k]
1015         else:
1016             raise ValueError("Load mode '%s' unknown. Choose another one."%mode)
1017         #
1018         return filename
1019
1020 # ==============================================================================
1021 if __name__ == "__main__":
1022     print('\n AUTODIAGNOSTIC\n')