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