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