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