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