Salome HOME
Minor documentation improvements
[modules/adao.git] / src / daComposant / daCore / Persistence.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2008-2021 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 name(self):
151         return self.__name
152
153     def __getitem__(self, index=None ):
154         "x.__getitem__(y) <==> x[y]"
155         return copy.copy(self.__values[index])
156
157     def count(self, value):
158         "L.count(value) -> integer -- return number of occurrences of value"
159         return self.__values.count(value)
160
161     def index(self, value, start=0, stop=None):
162         "L.index(value, [start, [stop]]) -> integer -- return first index of value."
163         if stop is None : stop = len(self.__values)
164         return self.__values.index(value, start, stop)
165
166     # ---------------------------------------------------------
167     def __filteredIndexes(self, **kwargs):
168         "Function interne filtrant les index"
169         __indexOfFilteredItems = range(len(self.__tags))
170         __filteringKwTags = kwargs.keys()
171         if len(__filteringKwTags) > 0:
172             for tagKey in __filteringKwTags:
173                 __tmp = []
174                 for i in __indexOfFilteredItems:
175                     if tagKey in self.__tags[i]:
176                         if self.__tags[i][tagKey] == kwargs[tagKey]:
177                             __tmp.append( i )
178                         elif isinstance(kwargs[tagKey],(list,tuple)) and self.__tags[i][tagKey] in kwargs[tagKey]:
179                             __tmp.append( i )
180                 __indexOfFilteredItems = __tmp
181                 if len(__indexOfFilteredItems) == 0: break
182         return __indexOfFilteredItems
183
184     # ---------------------------------------------------------
185     def values(self, **kwargs):
186         "D.values() -> list of D's values"
187         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
188         return [self.__values[i] for i in __indexOfFilteredItems]
189
190     def keys(self, keyword=None , **kwargs):
191         "D.keys() -> list of D's keys"
192         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
193         __keys = []
194         for i in __indexOfFilteredItems:
195             if keyword in self.__tags[i]:
196                 __keys.append( self.__tags[i][keyword] )
197             else:
198                 __keys.append( None )
199         return __keys
200
201     def items(self, keyword=None , **kwargs):
202         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
203         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
204         __pairs = []
205         for i in __indexOfFilteredItems:
206             if keyword in self.__tags[i]:
207                 __pairs.append( (self.__tags[i][keyword], self.__values[i]) )
208             else:
209                 __pairs.append( (None, self.__values[i]) )
210         return __pairs
211
212     def tagkeys(self):
213         "D.tagkeys() -> list of D's tag keys"
214         __allKeys = []
215         for dicotags in self.__tags:
216             __allKeys.extend( list(dicotags.keys()) )
217         __allKeys = sorted(set(__allKeys))
218         return __allKeys
219
220     # def valueserie(self, item=None, allSteps=True, **kwargs):
221     #     if item is not None:
222     #         return self.__values[item]
223     #     else:
224     #         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
225     #         if not allSteps and len(__indexOfFilteredItems) > 0:
226     #             return self.__values[__indexOfFilteredItems[0]]
227     #         else:
228     #             return [self.__values[i] for i in __indexOfFilteredItems]
229
230     def tagserie(self, item=None, withValues=False, outputTag=None, **kwargs):
231         "D.tagserie() -> list of D's tag serie"
232         if item is None:
233             __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
234         else:
235             __indexOfFilteredItems = [item,]
236         #
237         # Dans le cas où la sortie donne les valeurs d'un "outputTag"
238         if outputTag is not None and isinstance(outputTag,str) :
239             outputValues = []
240             for index in __indexOfFilteredItems:
241                 if outputTag in self.__tags[index].keys():
242                     outputValues.append( self.__tags[index][outputTag] )
243             outputValues = sorted(set(outputValues))
244             return outputValues
245         #
246         # Dans le cas où la sortie donne les tags satisfaisants aux conditions
247         else:
248             if withValues:
249                 return [self.__tags[index] for index in __indexOfFilteredItems]
250             else:
251                 allTags = {}
252                 for index in __indexOfFilteredItems:
253                     allTags.update( self.__tags[index] )
254                 allKeys = sorted(allTags.keys())
255                 return allKeys
256
257     # ---------------------------------------------------------
258     # Pour compatibilite
259     def stepnumber(self):
260         "Nombre de pas"
261         return len(self.__values)
262
263     # Pour compatibilite
264     def stepserie(self, **kwargs):
265         "Nombre de pas filtrés"
266         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
267         return __indexOfFilteredItems
268
269     # Pour compatibilite
270     def steplist(self, **kwargs):
271         "Nombre de pas filtrés"
272         __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
273         return list(__indexOfFilteredItems)
274
275     # ---------------------------------------------------------
276     def means(self):
277         """
278         Renvoie la série, contenant à chaque pas, la valeur moyenne des données
279         au pas. Il faut que le type de base soit compatible avec les types
280         élémentaires numpy.
281         """
282         try:
283             return [numpy.mean(item, dtype=mfp).astype('float') for item in self.__values]
284         except:
285             raise TypeError("Base type is incompatible with numpy")
286
287     def stds(self, ddof=0):
288         """
289         Renvoie la série, contenant à chaque pas, l'écart-type des données
290         au pas. Il faut que le type de base soit compatible avec les types
291         élémentaires numpy.
292
293         ddof : c'est le nombre de degrés de liberté pour le calcul de
294                l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
295         """
296         try:
297             if numpy.version.version >= '1.1.0':
298                 return [numpy.array(item).std(ddof=ddof, dtype=mfp).astype('float') for item in self.__values]
299             else:
300                 return [numpy.array(item).std(dtype=mfp).astype('float') for item in self.__values]
301         except:
302             raise TypeError("Base type is incompatible with numpy")
303
304     def sums(self):
305         """
306         Renvoie la série, contenant à chaque pas, la somme des données au pas.
307         Il faut que le type de base soit compatible avec les types élémentaires
308         numpy.
309         """
310         try:
311             return [numpy.array(item).sum() for item in self.__values]
312         except:
313             raise TypeError("Base type is incompatible with numpy")
314
315     def mins(self):
316         """
317         Renvoie la série, contenant à chaque pas, le minimum des données au pas.
318         Il faut que le type de base soit compatible avec les types élémentaires
319         numpy.
320         """
321         try:
322             return [numpy.array(item).min() for item in self.__values]
323         except:
324             raise TypeError("Base type is incompatible with numpy")
325
326     def maxs(self):
327         """
328         Renvoie la série, contenant à chaque pas, la maximum des données au pas.
329         Il faut que le type de base soit compatible avec les types élémentaires
330         numpy.
331         """
332         try:
333             return [numpy.array(item).max() for item in self.__values]
334         except:
335             raise TypeError("Base type is incompatible with numpy")
336
337     def __preplots(self,
338                    title    = "",
339                    xlabel   = "",
340                    ylabel   = "",
341                    ltitle   = None,
342                    geometry = "600x400",
343                    persist  = False,
344                    pause    = True,
345                   ):
346         "Préparation des plots"
347         #
348         # Vérification de la disponibilité du module Gnuplot
349         if not has_gnuplot:
350             raise ImportError("The Gnuplot module is required to plot the object.")
351         #
352         # Vérification et compléments sur les paramètres d'entrée
353         if persist:
354             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist -geometry '+geometry
355         else:
356             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -geometry '+geometry
357         if ltitle is None:
358             ltitle = ""
359         self.__g = Gnuplot.Gnuplot() # persist=1
360         self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term)
361         self.__g('set style data lines')
362         self.__g('set grid')
363         self.__g('set autoscale')
364         self.__g('set xlabel "'+str(xlabel)+'"')
365         self.__g('set ylabel "'+str(ylabel)+'"')
366         self.__title  = title
367         self.__ltitle = ltitle
368         self.__pause  = pause
369
370     def plots(self,
371               item     = None,
372               step     = None,
373               steps    = None,
374               title    = "",
375               xlabel   = "",
376               ylabel   = "",
377               ltitle   = None,
378               geometry = "600x400",
379               filename = "",
380               dynamic  = False,
381               persist  = False,
382               pause    = True,
383              ):
384         """
385         Renvoie un affichage de la valeur à chaque pas, si elle est compatible
386         avec un affichage Gnuplot (donc essentiellement un vecteur). Si
387         l'argument "step" existe dans la liste des pas de stockage effectués,
388         renvoie l'affichage de la valeur stockée à ce pas "step". Si l'argument
389         "item" est correct, renvoie l'affichage de la valeur stockée au numéro
390         "item". Par défaut ou en l'absence de "step" ou "item", renvoie un
391         affichage successif de tous les pas.
392
393         Arguments :
394             - step     : valeur du pas à afficher
395             - item     : index de la valeur à afficher
396             - steps    : liste unique des pas de l'axe des X, ou None si c'est
397                          la numérotation par défaut
398             - title    : base du titre général, qui sera automatiquement
399                          complétée par la mention du pas
400             - xlabel   : label de l'axe des X
401             - ylabel   : label de l'axe des Y
402             - ltitle   : titre associé au vecteur tracé
403             - geometry : taille en pixels de la fenêtre et position du coin haut
404                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
405             - filename : base de nom de fichier Postscript pour une sauvegarde,
406                          qui est automatiquement complétée par le numéro du
407                          fichier calculé par incrément simple de compteur
408             - dynamic  : effectue un affichage des valeurs à chaque stockage
409                          (au-delà du second). La méthode "plots" permet de
410                          déclarer l'affichage dynamique, et c'est la méthode
411                          "__replots" qui est utilisée pour l'effectuer
412             - persist  : booléen indiquant que la fenêtre affichée sera
413                          conservée lors du passage au dessin suivant
414                          Par défaut, persist = False
415             - pause    : booléen indiquant une pause après chaque tracé, et
416                          attendant un Return
417                          Par défaut, pause = True
418         """
419         if not self.__dynamic:
420             self.__preplots(title, xlabel, ylabel, ltitle, geometry, persist, pause )
421             if dynamic:
422                 self.__dynamic = True
423                 if len(self.__values) == 0: return 0
424         #
425         # Tracé du ou des vecteurs demandés
426         indexes = []
427         if step is not None and step < len(self.__values):
428             indexes.append(step)
429         elif item is not None and item < len(self.__values):
430             indexes.append(item)
431         else:
432             indexes = indexes + list(range(len(self.__values)))
433         #
434         i = -1
435         for index in indexes:
436             self.__g('set title  "'+str(title)+' (pas '+str(index)+')"')
437             if isinstance(steps,list) or isinstance(steps,numpy.ndarray):
438                 Steps = list(steps)
439             else:
440                 Steps = list(range(len(self.__values[index])))
441             #
442             self.__g.plot( Gnuplot.Data( Steps, self.__values[index], title=ltitle ) )
443             #
444             if filename != "":
445                 i += 1
446                 stepfilename = "%s_%03i.ps"%(filename,i)
447                 if os.path.isfile(stepfilename):
448                     raise ValueError("Error: a file with this name \"%s\" already exists."%stepfilename)
449                 self.__g.hardcopy(filename=stepfilename, color=1)
450             if self.__pause:
451                 eval(input('Please press return to continue...\n'))
452
453     def __replots(self):
454         """
455         Affichage dans le cas du suivi dynamique de la variable
456         """
457         if self.__dynamic and len(self.__values) < 2: return 0
458         #
459         self.__g('set title  "'+str(self.__title))
460         Steps = list(range(len(self.__values)))
461         self.__g.plot( Gnuplot.Data( Steps, self.__values, title=self.__ltitle ) )
462         #
463         if self.__pause:
464             eval(input('Please press return to continue...\n'))
465
466     # ---------------------------------------------------------
467     # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
468     def mean(self):
469         """
470         Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
471         longueur des pas. Il faut que le type de base soit compatible avec
472         les types élémentaires numpy.
473         """
474         try:
475             return numpy.mean(self.__values, axis=0, dtype=mfp).astype('float')
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).astype('float')
491             else:
492                 return numpy.array(self.__values).std(axis=0).astype('float')
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')