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