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