Salome HOME
Extend the range of supported versions for various modules (1)
[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.array(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.array(item).std(ddof=ddof) for item in self.__values]
295             else:
296                 return [numpy.array(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.array(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.array(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.array(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     # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
464     def mean(self):
465         """
466         Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
467         longueur des pas. Il faut que le type de base soit compatible avec
468         les types élémentaires numpy.
469         """
470         try:
471             if self.__basetype in [int, float]:
472                 return float( numpy.array(self.__values).mean() )
473             else:
474                 return numpy.array(self.__values).mean(axis=0)
475         except:
476             raise TypeError("Base type is incompatible with numpy")
477
478     def std(self, ddof=0):
479         """
480         Renvoie l'écart-type de toutes les valeurs sans tenir compte de la
481         longueur des pas. Il faut que le type de base soit compatible avec
482         les types élémentaires numpy.
483
484         ddof : c'est le nombre de degrés de liberté pour le calcul de
485                l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
486         """
487         try:
488             if numpy.version.version >= '1.1.0':
489                 return numpy.array(self.__values).std(ddof=ddof,axis=0)
490             else:
491                 return numpy.array(self.__values).std(axis=0)
492         except:
493             raise TypeError("Base type is incompatible with numpy")
494
495     def sum(self):
496         """
497         Renvoie la somme de toutes les valeurs sans tenir compte de la
498         longueur des pas. Il faut que le type de base soit compatible avec
499         les types élémentaires numpy.
500         """
501         try:
502             return numpy.array(self.__values).sum(axis=0)
503         except:
504             raise TypeError("Base type is incompatible with numpy")
505
506     def min(self):
507         """
508         Renvoie le minimum de toutes les valeurs sans tenir compte de la
509         longueur des pas. Il faut que le type de base soit compatible avec
510         les types élémentaires numpy.
511         """
512         try:
513             return numpy.array(self.__values).min(axis=0)
514         except:
515             raise TypeError("Base type is incompatible with numpy")
516
517     def max(self):
518         """
519         Renvoie le maximum de toutes les valeurs sans tenir compte de la
520         longueur des pas. Il faut que le type de base soit compatible avec
521         les types élémentaires numpy.
522         """
523         try:
524             return numpy.array(self.__values).max(axis=0)
525         except:
526             raise TypeError("Base type is incompatible with numpy")
527
528     def cumsum(self):
529         """
530         Renvoie la somme cumulée de toutes les valeurs sans tenir compte de la
531         longueur des pas. Il faut que le type de base soit compatible avec
532         les types élémentaires numpy.
533         """
534         try:
535             return numpy.array(self.__values).cumsum(axis=0)
536         except:
537             raise TypeError("Base type is incompatible with numpy")
538
539     def plot(self,
540              steps    = None,
541              title    = "",
542              xlabel   = "",
543              ylabel   = "",
544              ltitle   = None,
545              geometry = "600x400",
546              filename = "",
547              persist  = False,
548              pause    = True,
549             ):
550         """
551         Renvoie un affichage unique pour l'ensemble des valeurs à chaque pas, si
552         elles sont compatibles avec un affichage Gnuplot (donc essentiellement
553         un vecteur). Si l'argument "step" existe dans la liste des pas de
554         stockage effectués, renvoie l'affichage de la valeur stockée à ce pas
555         "step". Si l'argument "item" est correct, renvoie l'affichage de la
556         valeur stockée au numéro "item".
557
558         Arguments :
559             - steps    : liste unique des pas de l'axe des X, ou None si c'est
560                          la numérotation par défaut
561             - title    : base du titre général, qui sera automatiquement
562                          complétée par la mention du pas
563             - xlabel   : label de l'axe des X
564             - ylabel   : label de l'axe des Y
565             - ltitle   : titre associé au vecteur tracé
566             - geometry : taille en pixels de la fenêtre et position du coin haut
567                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
568             - filename : nom de fichier Postscript pour une sauvegarde
569             - persist  : booléen indiquant que la fenêtre affichée sera
570                          conservée lors du passage au dessin suivant
571                          Par défaut, persist = False
572             - pause    : booléen indiquant une pause après chaque tracé, et
573                          attendant un Return
574                          Par défaut, pause = True
575         """
576         #
577         # Vérification de la disponibilité du module Gnuplot
578         if not has_gnuplot:
579             raise ImportError("The Gnuplot module is required to plot the object.")
580         #
581         # Vérification et compléments sur les paramètres d'entrée
582         if persist:
583             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist -geometry '+geometry
584         else:
585             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -geometry '+geometry
586         if ltitle is None:
587             ltitle = ""
588         if isinstance(steps,list) or isinstance(steps, numpy.ndarray):
589             Steps = list(steps)
590         else:
591             Steps = list(range(len(self.__values[0])))
592         self.__g = Gnuplot.Gnuplot() # persist=1
593         self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term)
594         self.__g('set style data lines')
595         self.__g('set grid')
596         self.__g('set autoscale')
597         self.__g('set title  "'+str(title) +'"')
598         self.__g('set xlabel "'+str(xlabel)+'"')
599         self.__g('set ylabel "'+str(ylabel)+'"')
600         #
601         # Tracé du ou des vecteurs demandés
602         indexes = list(range(len(self.__values)))
603         self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle+" (pas 0)" ) )
604         for index in indexes:
605             self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle+" (pas %i)"%index ) )
606         #
607         if filename != "":
608             self.__g.hardcopy(filename=filename, color=1)
609         if pause:
610             eval(input('Please press return to continue...\n'))
611
612     # ---------------------------------------------------------
613     def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None):
614         """
615         Association à la variable d'un triplet définissant un observer
616
617         Le Scheduler attendu est une fréquence, une simple liste d'index ou un
618         range des index.
619         """
620         #
621         # Vérification du Scheduler
622         # -------------------------
623         maxiter = int( 1e9 )
624         if isinstance(Scheduler,int):      # Considéré comme une fréquence à partir de 0
625             Schedulers = range( 0, maxiter, int(Scheduler) )
626         elif isinstance(Scheduler,range):  # Considéré comme un itérateur
627             Schedulers = Scheduler
628         elif isinstance(Scheduler,(list,tuple)):   # Considéré comme des index explicites
629             Schedulers = [iLong(i) for i in Scheduler] # map( long, Scheduler )
630         else:                              # Dans tous les autres cas, activé par défaut
631             Schedulers = range( 0, maxiter )
632         #
633         # Stockage interne de l'observer dans la variable
634         # -----------------------------------------------
635         self.__dataobservers.append( [HookFunction, HookParameters, Schedulers] )
636
637     def removeDataObserver(self, HookFunction = None, AllObservers = False):
638         """
639         Suppression d'un observer nommé sur la variable.
640
641         On peut donner dans HookFunction la meme fonction que lors de la
642         définition, ou un simple string qui est le nom de la fonction. Si
643         AllObservers est vrai, supprime tous les observers enregistrés.
644         """
645         if hasattr(HookFunction,"func_name"):
646             name = str( HookFunction.func_name )
647         elif hasattr(HookFunction,"__name__"):
648             name = str( HookFunction.__name__ )
649         elif isinstance(HookFunction,str):
650             name = str( HookFunction )
651         else:
652             name = None
653         #
654         i = -1
655         index_to_remove = []
656         for [hf, hp, hs] in self.__dataobservers:
657             i = i + 1
658             if name is hf.__name__ or AllObservers: index_to_remove.append( i )
659         index_to_remove.reverse()
660         for i in index_to_remove:
661             self.__dataobservers.pop( i )
662         return len(index_to_remove)
663
664     def hasDataObserver(self):
665         return bool(len(self.__dataobservers) > 0)
666
667 # ==============================================================================
668 class SchedulerTrigger(object):
669     """
670     Classe générale d'interface de type Scheduler/Trigger
671     """
672     def __init__(self,
673                  simplifiedCombo = None,
674                  startTime       = 0,
675                  endTime         = int( 1e9 ),
676                  timeDelay       = 1,
677                  timeUnit        = 1,
678                  frequency       = None,
679                 ):
680         pass
681
682 # ==============================================================================
683 class OneScalar(Persistence):
684     """
685     Classe définissant le stockage d'une valeur unique réelle (float) par pas.
686
687     Le type de base peut être changé par la méthode "basetype", mais il faut que
688     le nouveau type de base soit compatible avec les types par éléments de
689     numpy. On peut même utiliser cette classe pour stocker des vecteurs/listes
690     ou des matrices comme dans les classes suivantes, mais c'est déconseillé
691     pour conserver une signification claire des noms.
692     """
693     def __init__(self, name="", unit="", basetype = float):
694         Persistence.__init__(self, name, unit, basetype)
695
696 class OneIndex(Persistence):
697     """
698     Classe définissant le stockage d'une valeur unique entière (int) par pas.
699     """
700     def __init__(self, name="", unit="", basetype = int):
701         Persistence.__init__(self, name, unit, basetype)
702
703 class OneVector(Persistence):
704     """
705     Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
706     pas utiliser cette classe pour des données hétérogènes, mais "OneList".
707     """
708     def __init__(self, name="", unit="", basetype = numpy.ravel):
709         Persistence.__init__(self, name, unit, basetype)
710
711 class OneMatrix(Persistence):
712     """
713     Classe de stockage d'une matrice de valeurs (numpy.matrix) par pas.
714     """
715     def __init__(self, name="", unit="", basetype = numpy.matrix):
716         Persistence.__init__(self, name, unit, basetype)
717
718 class OneList(Persistence):
719     """
720     Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne pas
721     utiliser cette classe pour des données numériques homogènes, mais
722     "OneVector".
723     """
724     def __init__(self, name="", unit="", basetype = list):
725         Persistence.__init__(self, name, unit, basetype)
726
727 def NoType( value ):
728     "Fonction transparente, sans effet sur son argument"
729     return value
730
731 class OneNoType(Persistence):
732     """
733     Classe de stockage d'un objet sans modification (cast) de type. Attention,
734     selon le véritable type de l'objet stocké à chaque pas, les opérations
735     arithmétiques à base de numpy peuvent être invalides ou donner des résultats
736     inattendus. Cette classe n'est donc à utiliser qu'à bon escient
737     volontairement, et pas du tout par défaut.
738     """
739     def __init__(self, name="", unit="", basetype = NoType):
740         Persistence.__init__(self, name, unit, basetype)
741
742 # ==============================================================================
743 class CompositePersistence(object):
744     """
745     Structure de stockage permettant de rassembler plusieurs objets de
746     persistence.
747
748     Des objets par défaut sont prévus, et des objets supplémentaires peuvent
749     être ajoutés.
750     """
751     def __init__(self, name="", defaults=True):
752         """
753         name : nom courant
754
755         La gestion interne des données est exclusivement basée sur les variables
756         initialisées ici (qui ne sont pas accessibles depuis l'extérieur des
757         objets comme des attributs) :
758         __StoredObjects : objets de type persistence collectés dans cet objet
759         """
760         self.__name = str(name)
761         #
762         self.__StoredObjects = {}
763         #
764         # Definition des objets par defaut
765         # --------------------------------
766         if defaults:
767             self.__StoredObjects["Informations"]     = OneNoType("Informations")
768             self.__StoredObjects["Background"]       = OneVector("Background", basetype=numpy.array)
769             self.__StoredObjects["BackgroundError"]  = OneMatrix("BackgroundError")
770             self.__StoredObjects["Observation"]      = OneVector("Observation", basetype=numpy.array)
771             self.__StoredObjects["ObservationError"] = OneMatrix("ObservationError")
772             self.__StoredObjects["Analysis"]         = OneVector("Analysis", basetype=numpy.array)
773             self.__StoredObjects["AnalysisError"]    = OneMatrix("AnalysisError")
774             self.__StoredObjects["Innovation"]       = OneVector("Innovation", basetype=numpy.array)
775             self.__StoredObjects["KalmanGainK"]      = OneMatrix("KalmanGainK")
776             self.__StoredObjects["OperatorH"]        = OneMatrix("OperatorH")
777             self.__StoredObjects["RmsOMA"]           = OneScalar("RmsOMA")
778             self.__StoredObjects["RmsOMB"]           = OneScalar("RmsOMB")
779             self.__StoredObjects["RmsBMA"]           = OneScalar("RmsBMA")
780         #
781
782     def store(self, name=None, value=None, **kwargs):
783         """
784         Stockage d'une valeur "value" pour le "step" dans la variable "name".
785         """
786         if name is None: raise ValueError("Storable object name is required for storage.")
787         if name not in self.__StoredObjects.keys():
788             raise ValueError("No such name '%s' exists in storable objects."%name)
789         self.__StoredObjects[name].store( value=value, **kwargs )
790
791     def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
792         """
793         Ajoute dans les objets stockables un nouvel objet défini par son nom, son
794         type de Persistence et son type de base à chaque pas.
795         """
796         if name is None: raise ValueError("Object name is required for adding an object.")
797         if name in self.__StoredObjects.keys():
798             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
799         if basetype is None:
800             self.__StoredObjects[name] = persistenceType( name=str(name) )
801         else:
802             self.__StoredObjects[name] = persistenceType( name=str(name), basetype=basetype )
803
804     def get_object(self, name=None ):
805         """
806         Renvoie l'objet de type Persistence qui porte le nom demandé.
807         """
808         if name is None: raise ValueError("Object name is required for retrieving an object.")
809         if name not in self.__StoredObjects.keys():
810             raise ValueError("No such name '%s' exists in stored objects."%name)
811         return self.__StoredObjects[name]
812
813     def set_object(self, name=None, objet=None ):
814         """
815         Affecte directement un 'objet' qui porte le nom 'name' demandé.
816         Attention, il n'est pas effectué de vérification sur le type, qui doit
817         comporter les méthodes habituelles de Persistence pour que cela
818         fonctionne.
819         """
820         if name is None: raise ValueError("Object name is required for setting an object.")
821         if name in self.__StoredObjects.keys():
822             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
823         self.__StoredObjects[name] = objet
824
825     def del_object(self, name=None ):
826         """
827         Supprime un objet de la liste des objets stockables.
828         """
829         if name is None: raise ValueError("Object name is required for retrieving an object.")
830         if name not in self.__StoredObjects.keys():
831             raise ValueError("No such name '%s' exists in stored objects."%name)
832         del self.__StoredObjects[name]
833
834     # ---------------------------------------------------------
835     # Méthodes d'accès de type dictionnaire
836     def __getitem__(self, name=None ):
837         "x.__getitem__(y) <==> x[y]"
838         return self.get_object( name )
839
840     def __setitem__(self, name=None, objet=None ):
841         "x.__setitem__(i, y) <==> x[i]=y"
842         self.set_object( name, objet )
843
844     def keys(self):
845         "D.keys() -> list of D's keys"
846         return self.get_stored_objects(hideVoidObjects = False)
847
848     def values(self):
849         "D.values() -> list of D's values"
850         return self.__StoredObjects.values()
851
852     def items(self):
853         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
854         return self.__StoredObjects.items()
855
856     # ---------------------------------------------------------
857     def get_stored_objects(self, hideVoidObjects = False):
858         "Renvoie la liste des objets présents"
859         objs = self.__StoredObjects.keys()
860         if hideVoidObjects:
861             usedObjs = []
862             for k in objs:
863                 try:
864                     if len(self.__StoredObjects[k]) > 0: usedObjs.append( k )
865                 finally:
866                     pass
867             objs = usedObjs
868         objs = sorted(objs)
869         return objs
870
871     # ---------------------------------------------------------
872     def save_composite(self, filename=None, mode="pickle", compress="gzip"):
873         """
874         Enregistre l'objet dans le fichier indiqué selon le "mode" demandé,
875         et renvoi le nom du fichier
876         """
877         if filename is None:
878             if compress == "gzip":
879                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.gz"
880             elif compress == "bzip2":
881                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.bz2"
882             else:
883                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl"
884         else:
885             filename = os.path.abspath( filename )
886         #
887         if mode == "pickle":
888             if compress == "gzip":
889                 output = gzip.open( filename, 'wb')
890             elif compress == "bzip2":
891                 output = bz2.BZ2File( filename, 'wb')
892             else:
893                 output = open( filename, 'wb')
894             pickle.dump(self, output)
895             output.close()
896         else:
897             raise ValueError("Save mode '%s' unknown. Choose another one."%mode)
898         #
899         return filename
900
901     def load_composite(self, filename=None, mode="pickle", compress="gzip"):
902         """
903         Recharge un objet composite sauvé en fichier
904         """
905         if filename is None:
906             raise ValueError("A file name if requested to load a composite.")
907         else:
908             filename = os.path.abspath( filename )
909         #
910         if mode == "pickle":
911             if compress == "gzip":
912                 pkl_file = gzip.open( filename, 'rb')
913             elif compress == "bzip2":
914                 pkl_file = bz2.BZ2File( filename, 'rb')
915             else:
916                 pkl_file = open(filename, 'rb')
917             output = pickle.load(pkl_file)
918             for k in output.keys():
919                 self[k] = output[k]
920         else:
921             raise ValueError("Load mode '%s' unknown. Choose another one."%mode)
922         #
923         return filename
924
925 # ==============================================================================
926 if __name__ == "__main__":
927     print('\n AUTODIAGNOSTIC\n')