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