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