]> SALOME platform Git repositories - modules/adao.git/blob - src/daComposant/daCore/Persistence.py
Salome HOME
Documentation corrections and code performance update
[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 traces(self, offset=0):
359         """
360         Trace
361
362         Renvoie la série contenant, à chaque pas, la trace (avec l'offset) des
363         données au pas. Il faut que le type de base soit compatible avec les
364         types élémentaires numpy.
365         """
366         try:
367             return [numpy.trace(item, offset, dtype=mfp) for item in self.__values]
368         except Exception:
369             raise TypeError("Base type is incompatible with numpy")
370
371     def maes(self, _predictor=None):
372         """
373         Mean Absolute Error (MAE)
374         mae(dX) = 1/n sum(dX_i)
375
376         Renvoie la série contenant, à chaque pas, la MAE des données au pas.
377         Il faut que le type de base soit compatible avec les types élémentaires
378         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
379         prédicteur est None, sinon c'est appliqué à l'écart entre les données
380         au pas et le prédicteur au même pas.
381         """
382         if _predictor is None:
383             try:
384                 return [numpy.mean(numpy.abs(item)) for item in self.__values]
385             except Exception:
386                 raise TypeError("Base type is incompatible with numpy")
387         else:
388             if len(_predictor) != len(self.__values):
389                 raise ValueError("Predictor number of steps is incompatible with the values")
390             for i, item in enumerate(self.__values):
391                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
392                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
393             try:
394                 return [numpy.mean(numpy.abs(numpy.ravel(item) - numpy.ravel(_predictor[i]))) for i, item in enumerate(self.__values)]
395             except Exception:
396                 raise TypeError("Base type is incompatible with numpy")
397
398     def mses(self, _predictor=None):
399         """
400         Mean-Square Error (MSE) ou Mean-Square Deviation (MSD)
401         mse(dX) = 1/n sum(dX_i**2)
402
403         Renvoie la série contenant, à chaque pas, la MSE des données au pas. Il
404         faut que le type de base soit compatible avec les types élémentaires
405         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
406         prédicteur est None, sinon c'est appliqué à l'écart entre les données
407         au pas et le prédicteur au même pas.
408         """
409         if _predictor is None:
410             try:
411                 __n = self.shape()[0]
412                 return [(numpy.linalg.norm(item)**2 / __n) for item in self.__values]
413             except Exception:
414                 raise TypeError("Base type is incompatible with numpy")
415         else:
416             if len(_predictor) != len(self.__values):
417                 raise ValueError("Predictor number of steps is incompatible with the values")
418             for i, item in enumerate(self.__values):
419                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
420                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
421             try:
422                 __n = self.shape()[0]
423                 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i]))**2 / __n) for i, item in enumerate(self.__values)]
424             except Exception:
425                 raise TypeError("Base type is incompatible with numpy")
426
427     msds=mses # Mean-Square Deviation (MSD=MSE)
428
429     def rmses(self, _predictor=None):
430         """
431         Root-Mean-Square Error (RMSE) ou Root-Mean-Square Deviation (RMSD)
432         rmse(dX) = sqrt( 1/n sum(dX_i**2) ) = sqrt( mse(dX) )
433
434         Renvoie la série contenant, à chaque pas, la RMSE des données au pas.
435         Il faut que le type de base soit compatible avec les types élémentaires
436         numpy. C'est réservé aux variables d'écarts ou d'incréments si le
437         prédicteur est None, sinon c'est appliqué à l'écart entre les données
438         au pas et le prédicteur au même pas.
439         """
440         if _predictor is None:
441             try:
442                 __n = self.shape()[0]
443                 return [(numpy.linalg.norm(item) / math.sqrt(__n)) for item in self.__values]
444             except Exception:
445                 raise TypeError("Base type is incompatible with numpy")
446         else:
447             if len(_predictor) != len(self.__values):
448                 raise ValueError("Predictor number of steps is incompatible with the values")
449             for i, item in enumerate(self.__values):
450                 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
451                     raise ValueError("Predictor size at step %i is incompatible with the values"%i)
452             try:
453                 __n = self.shape()[0]
454                 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i])) / math.sqrt(__n)) for i, item in enumerate(self.__values)]
455             except Exception:
456                 raise TypeError("Base type is incompatible with numpy")
457
458     rmsds = rmses # Root-Mean-Square Deviation (RMSD=RMSE)
459
460     def __preplots(self,
461                    title    = "",
462                    xlabel   = "",
463                    ylabel   = "",
464                    ltitle   = None,
465                    geometry = "600x400",
466                    persist  = False,
467                    pause    = True,
468                   ):
469         "Préparation des plots"
470         #
471         # Vérification de la disponibilité du module Gnuplot
472         if not has_gnuplot:
473             raise ImportError("The Gnuplot module is required to plot the object.")
474         #
475         # Vérification et compléments sur les paramètres d'entrée
476         if ltitle is None: ltitle = ""
477         __geometry = str(geometry)
478         __sizespec = (__geometry.split('+')[0]).replace('x',',')
479         #
480         if persist:
481             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
482         #
483         self.__g = Gnuplot.Gnuplot() # persist=1
484         self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term+' size '+__sizespec)
485         self.__g('set style data lines')
486         self.__g('set grid')
487         self.__g('set autoscale')
488         self.__g('set xlabel "'+str(xlabel)+'"')
489         self.__g('set ylabel "'+str(ylabel)+'"')
490         self.__title  = title
491         self.__ltitle = ltitle
492         self.__pause  = pause
493
494     def plots(self,
495               item     = None,
496               step     = None,
497               steps    = None,
498               title    = "",
499               xlabel   = "",
500               ylabel   = "",
501               ltitle   = None,
502               geometry = "600x400",
503               filename = "",
504               dynamic  = False,
505               persist  = False,
506               pause    = True,
507              ):
508         """
509         Renvoie un affichage de la valeur à chaque pas, si elle est compatible
510         avec un affichage Gnuplot (donc essentiellement un vecteur). Si
511         l'argument "step" existe dans la liste des pas de stockage effectués,
512         renvoie l'affichage de la valeur stockée à ce pas "step". Si l'argument
513         "item" est correct, renvoie l'affichage de la valeur stockée au numéro
514         "item". Par défaut ou en l'absence de "step" ou "item", renvoie un
515         affichage successif de tous les pas.
516
517         Arguments :
518             - step     : valeur du pas à afficher
519             - item     : index de la valeur à afficher
520             - steps    : liste unique des pas de l'axe des X, ou None si c'est
521                          la numérotation par défaut
522             - title    : base du titre général, qui sera automatiquement
523                          complétée par la mention du pas
524             - xlabel   : label de l'axe des X
525             - ylabel   : label de l'axe des Y
526             - ltitle   : titre associé au vecteur tracé
527             - geometry : taille en pixels de la fenêtre et position du coin haut
528                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
529             - filename : base de nom de fichier Postscript pour une sauvegarde,
530                          qui est automatiquement complétée par le numéro du
531                          fichier calculé par incrément simple de compteur
532             - dynamic  : effectue un affichage des valeurs à chaque stockage
533                          (au-delà du second). La méthode "plots" permet de
534                          déclarer l'affichage dynamique, et c'est la méthode
535                          "__replots" qui est utilisée pour l'effectuer
536             - persist  : booléen indiquant que la fenêtre affichée sera
537                          conservée lors du passage au dessin suivant
538                          Par défaut, persist = False
539             - pause    : booléen indiquant une pause après chaque tracé, et
540                          attendant un Return
541                          Par défaut, pause = True
542         """
543         if not self.__dynamic:
544             self.__preplots(title, xlabel, ylabel, ltitle, geometry, persist, pause )
545             if dynamic:
546                 self.__dynamic = True
547                 if len(self.__values) == 0: return 0
548         #
549         # Tracé du ou des vecteurs demandés
550         indexes = []
551         if step is not None and step < len(self.__values):
552             indexes.append(step)
553         elif item is not None and item < len(self.__values):
554             indexes.append(item)
555         else:
556             indexes = indexes + list(range(len(self.__values)))
557         #
558         i = -1
559         for index in indexes:
560             self.__g('set title  "'+str(title)+' (pas '+str(index)+')"')
561             if isinstance(steps, (list, numpy.ndarray)):
562                 Steps = list(steps)
563             else:
564                 Steps = list(range(len(self.__values[index])))
565             #
566             self.__g.plot( Gnuplot.Data( Steps, self.__values[index], title=ltitle ) )
567             #
568             if filename != "":
569                 i += 1
570                 stepfilename = "%s_%03i.ps"%(filename,i)
571                 if os.path.isfile(stepfilename):
572                     raise ValueError("Error: a file with this name \"%s\" already exists."%stepfilename)
573                 self.__g.hardcopy(filename=stepfilename, color=1)
574             if self.__pause:
575                 eval(input('Please press return to continue...\n'))
576
577     def __replots(self):
578         """
579         Affichage dans le cas du suivi dynamique de la variable
580         """
581         if self.__dynamic and len(self.__values) < 2: return 0
582         #
583         self.__g('set title  "'+str(self.__title))
584         Steps = list(range(len(self.__values)))
585         self.__g.plot( Gnuplot.Data( Steps, self.__values, title=self.__ltitle ) )
586         #
587         if self.__pause:
588             eval(input('Please press return to continue...\n'))
589
590     # ---------------------------------------------------------
591     # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
592     def mean(self):
593         """
594         Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
595         longueur des pas. Il faut que le type de base soit compatible avec
596         les types élémentaires numpy.
597         """
598         try:
599             return numpy.mean(self.__values, axis=0, dtype=mfp).astype('float')
600         except Exception:
601             raise TypeError("Base type is incompatible with numpy")
602
603     def std(self, ddof=0):
604         """
605         Renvoie l'écart-type de toutes les valeurs sans tenir compte de la
606         longueur des pas. Il faut que le type de base soit compatible avec
607         les types élémentaires numpy.
608
609         ddof : c'est le nombre de degrés de liberté pour le calcul de
610                l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
611         """
612         try:
613             if numpy.version.version >= '1.1.0':
614                 return numpy.asarray(self.__values).std(ddof=ddof,axis=0).astype('float')
615             else:
616                 return numpy.asarray(self.__values).std(axis=0).astype('float')
617         except Exception:
618             raise TypeError("Base type is incompatible with numpy")
619
620     def sum(self):
621         """
622         Renvoie la somme de toutes les valeurs sans tenir compte de la
623         longueur des pas. Il faut que le type de base soit compatible avec
624         les types élémentaires numpy.
625         """
626         try:
627             return numpy.asarray(self.__values).sum(axis=0)
628         except Exception:
629             raise TypeError("Base type is incompatible with numpy")
630
631     def min(self):
632         """
633         Renvoie le minimum de toutes les valeurs sans tenir compte de la
634         longueur des pas. Il faut que le type de base soit compatible avec
635         les types élémentaires numpy.
636         """
637         try:
638             return numpy.asarray(self.__values).min(axis=0)
639         except Exception:
640             raise TypeError("Base type is incompatible with numpy")
641
642     def max(self):
643         """
644         Renvoie le maximum de toutes les valeurs sans tenir compte de la
645         longueur des pas. Il faut que le type de base soit compatible avec
646         les types élémentaires numpy.
647         """
648         try:
649             return numpy.asarray(self.__values).max(axis=0)
650         except Exception:
651             raise TypeError("Base type is incompatible with numpy")
652
653     def cumsum(self):
654         """
655         Renvoie la somme cumulée de toutes les valeurs sans tenir compte de la
656         longueur des pas. Il faut que le type de base soit compatible avec
657         les types élémentaires numpy.
658         """
659         try:
660             return numpy.asarray(self.__values).cumsum(axis=0)
661         except Exception:
662             raise TypeError("Base type is incompatible with numpy")
663
664     def plot(self,
665              steps    = None,
666              title    = "",
667              xlabel   = "",
668              ylabel   = "",
669              ltitle   = None,
670              geometry = "600x400",
671              filename = "",
672              persist  = False,
673              pause    = True,
674             ):
675         """
676         Renvoie un affichage unique pour l'ensemble des valeurs à chaque pas, si
677         elles sont compatibles avec un affichage Gnuplot (donc essentiellement
678         un vecteur). Si l'argument "step" existe dans la liste des pas de
679         stockage effectués, renvoie l'affichage de la valeur stockée à ce pas
680         "step". Si l'argument "item" est correct, renvoie l'affichage de la
681         valeur stockée au numéro "item".
682
683         Arguments :
684             - steps    : liste unique des pas de l'axe des X, ou None si c'est
685                          la numérotation par défaut
686             - title    : base du titre général, qui sera automatiquement
687                          complétée par la mention du pas
688             - xlabel   : label de l'axe des X
689             - ylabel   : label de l'axe des Y
690             - ltitle   : titre associé au vecteur tracé
691             - geometry : taille en pixels de la fenêtre et position du coin haut
692                          gauche, au format X11 : LxH+X+Y (défaut : 600x400)
693             - filename : nom de fichier Postscript pour une sauvegarde
694             - persist  : booléen indiquant que la fenêtre affichée sera
695                          conservée lors du passage au dessin suivant
696                          Par défaut, persist = False
697             - pause    : booléen indiquant une pause après chaque tracé, et
698                          attendant un Return
699                          Par défaut, pause = True
700         """
701         #
702         # Vérification de la disponibilité du module Gnuplot
703         if not has_gnuplot:
704             raise ImportError("The Gnuplot module is required to plot the object.")
705         #
706         # Vérification et compléments sur les paramètres d'entrée
707         if ltitle is None: ltitle = ""
708         if isinstance(steps, (list, numpy.ndarray)):
709             Steps = list(steps)
710         else:
711             Steps = list(range(len(self.__values[0])))
712         __geometry = str(geometry)
713         __sizespec = (__geometry.split('+')[0]).replace('x',',')
714         #
715         if persist:
716             Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
717         #
718         self.__g = Gnuplot.Gnuplot() # persist=1
719         self.__g('set terminal '+Gnuplot.GnuplotOpts.default_term+' size '+__sizespec)
720         self.__g('set style data lines')
721         self.__g('set grid')
722         self.__g('set autoscale')
723         self.__g('set title  "'+str(title) +'"')
724         self.__g('set xlabel "'+str(xlabel)+'"')
725         self.__g('set ylabel "'+str(ylabel)+'"')
726         #
727         # Tracé du ou des vecteurs demandés
728         indexes = list(range(len(self.__values)))
729         self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle+" (pas 0)" ) )
730         for index in indexes:
731             self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle+" (pas %i)"%index ) )
732         #
733         if filename != "":
734             self.__g.hardcopy(filename=filename, color=1)
735         if pause:
736             eval(input('Please press return to continue...\n'))
737
738     # ---------------------------------------------------------
739     def s2mvr(self):
740         """
741         Renvoie la série sous la forme d'une unique matrice avec les données au
742         pas rangées par ligne
743         """
744         try:
745             return numpy.asarray(self.__values)
746         except Exception:
747             raise TypeError("Base type is incompatible with numpy")
748
749     def s2mvc(self):
750         """
751         Renvoie la série sous la forme d'une unique matrice avec les données au
752         pas rangées par colonne
753         """
754         try:
755             return numpy.asarray(self.__values).transpose()
756             # Eqvlt: return numpy.stack([numpy.ravel(sv) for sv in self.__values], axis=1)
757         except Exception:
758             raise TypeError("Base type is incompatible with numpy")
759
760     # ---------------------------------------------------------
761     def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None):
762         """
763         Association à la variable d'un triplet définissant un observer
764
765         Le Scheduler attendu est une fréquence, une simple liste d'index ou un
766         range des index.
767         """
768         #
769         # Vérification du Scheduler
770         # -------------------------
771         maxiter = int( 1e9 )
772         if isinstance(Scheduler,int):      # Considéré comme une fréquence à partir de 0
773             Schedulers = range( 0, maxiter, int(Scheduler) )
774         elif isinstance(Scheduler,range):  # Considéré comme un itérateur
775             Schedulers = Scheduler
776         elif isinstance(Scheduler,(list,tuple)):   # Considéré comme des index explicites
777             Schedulers = [int(i) for i in Scheduler] # map( long, Scheduler )
778         else:                              # Dans tous les autres cas, activé par défaut
779             Schedulers = range( 0, maxiter )
780         #
781         # Stockage interne de l'observer dans la variable
782         # -----------------------------------------------
783         self.__dataobservers.append( [HookFunction, HookParameters, Schedulers] )
784
785     def removeDataObserver(self, HookFunction = None, AllObservers = False):
786         """
787         Suppression d'un observer nommé sur la variable.
788
789         On peut donner dans HookFunction la meme fonction que lors de la
790         définition, ou un simple string qui est le nom de la fonction. Si
791         AllObservers est vrai, supprime tous les observers enregistrés.
792         """
793         if hasattr(HookFunction,"func_name"):
794             name = str( HookFunction.func_name )
795         elif hasattr(HookFunction,"__name__"):
796             name = str( HookFunction.__name__ )
797         elif isinstance(HookFunction,str):
798             name = str( HookFunction )
799         else:
800             name = None
801         #
802         i = -1
803         index_to_remove = []
804         for [hf, hp, hs] in self.__dataobservers:
805             i = i + 1
806             if name is hf.__name__ or AllObservers: index_to_remove.append( i )
807         index_to_remove.reverse()
808         for i in index_to_remove:
809             self.__dataobservers.pop( i )
810         return len(index_to_remove)
811
812     def hasDataObserver(self):
813         return bool(len(self.__dataobservers) > 0)
814
815 # ==============================================================================
816 class SchedulerTrigger(object):
817     """
818     Classe générale d'interface de type Scheduler/Trigger
819     """
820     __slots__ = ()
821     #
822     def __init__(self,
823                  simplifiedCombo = None,
824                  startTime       = 0,
825                  endTime         = int( 1e9 ),
826                  timeDelay       = 1,
827                  timeUnit        = 1,
828                  frequency       = None,
829                 ):
830         pass
831
832 # ==============================================================================
833 class OneScalar(Persistence):
834     """
835     Classe définissant le stockage d'une valeur unique réelle (float) par pas.
836
837     Le type de base peut être changé par la méthode "basetype", mais il faut que
838     le nouveau type de base soit compatible avec les types par éléments de
839     numpy. On peut même utiliser cette classe pour stocker des vecteurs/listes
840     ou des matrices comme dans les classes suivantes, mais c'est déconseillé
841     pour conserver une signification claire des noms.
842     """
843     __slots__ = ()
844     #
845     def __init__(self, name="", unit="", basetype = float):
846         Persistence.__init__(self, name, unit, basetype)
847
848 class OneIndex(Persistence):
849     """
850     Classe définissant le stockage d'une valeur unique entière (int) par pas.
851     """
852     __slots__ = ()
853     #
854     def __init__(self, name="", unit="", basetype = int):
855         Persistence.__init__(self, name, unit, basetype)
856
857 class OneVector(Persistence):
858     """
859     Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
860     pas utiliser cette classe pour des données hétérogènes, mais "OneList".
861     """
862     __slots__ = ()
863     #
864     def __init__(self, name="", unit="", basetype = numpy.ravel):
865         Persistence.__init__(self, name, unit, basetype)
866
867 class OneMatrice(Persistence):
868     """
869     Classe de stockage d'une matrice de valeurs homogènes par pas.
870     """
871     __slots__ = ()
872     #
873     def __init__(self, name="", unit="", basetype = numpy.array):
874         Persistence.__init__(self, name, unit, basetype)
875
876 class OneMatrix(Persistence):
877     """
878     Classe de stockage d'une matrice de valeurs homogènes par pas.
879     """
880     __slots__ = ()
881     #
882     def __init__(self, name="", unit="", basetype = numpy.matrix):
883         Persistence.__init__(self, name, unit, basetype)
884
885 class OneList(Persistence):
886     """
887     Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne
888     pas utiliser cette classe pour des données numériques homogènes, mais
889     "OneVector".
890     """
891     __slots__ = ()
892     #
893     def __init__(self, name="", unit="", basetype = list):
894         Persistence.__init__(self, name, unit, basetype)
895
896 def NoType( value ):
897     "Fonction transparente, sans effet sur son argument"
898     return value
899
900 class OneNoType(Persistence):
901     """
902     Classe de stockage d'un objet sans modification (cast) de type. Attention,
903     selon le véritable type de l'objet stocké à chaque pas, les opérations
904     arithmétiques à base de numpy peuvent être invalides ou donner des
905     résultats inattendus. Cette classe n'est donc à utiliser qu'à bon escient
906     volontairement, et pas du tout par défaut.
907     """
908     __slots__ = ()
909     #
910     def __init__(self, name="", unit="", basetype = NoType):
911         Persistence.__init__(self, name, unit, basetype)
912
913 # ==============================================================================
914 class CompositePersistence(object):
915     """
916     Structure de stockage permettant de rassembler plusieurs objets de
917     persistence.
918
919     Des objets par défaut sont prévus, et des objets supplémentaires peuvent
920     être ajoutés.
921     """
922     __slots__ = ("__name", "__StoredObjects")
923     #
924     def __init__(self, name="", defaults=True):
925         """
926         name : nom courant
927
928         La gestion interne des données est exclusivement basée sur les
929         variables initialisées ici (qui ne sont pas accessibles depuis
930         l'extérieur des objets comme des attributs) :
931         __StoredObjects : objets de type persistence collectés dans cet objet
932         """
933         self.__name = str(name)
934         #
935         self.__StoredObjects = {}
936         #
937         # Definition des objets par defaut
938         # --------------------------------
939         if defaults:
940             self.__StoredObjects["Informations"]     = OneNoType("Informations")
941             self.__StoredObjects["Background"]       = OneVector("Background", basetype=numpy.array)
942             self.__StoredObjects["BackgroundError"]  = OneMatrix("BackgroundError")
943             self.__StoredObjects["Observation"]      = OneVector("Observation", basetype=numpy.array)
944             self.__StoredObjects["ObservationError"] = OneMatrix("ObservationError")
945             self.__StoredObjects["Analysis"]         = OneVector("Analysis", basetype=numpy.array)
946             self.__StoredObjects["AnalysisError"]    = OneMatrix("AnalysisError")
947             self.__StoredObjects["Innovation"]       = OneVector("Innovation", basetype=numpy.array)
948             self.__StoredObjects["KalmanGainK"]      = OneMatrix("KalmanGainK")
949             self.__StoredObjects["OperatorH"]        = OneMatrix("OperatorH")
950             self.__StoredObjects["RmsOMA"]           = OneScalar("RmsOMA")
951             self.__StoredObjects["RmsOMB"]           = OneScalar("RmsOMB")
952             self.__StoredObjects["RmsBMA"]           = OneScalar("RmsBMA")
953         #
954
955     def store(self, name=None, value=None, **kwargs):
956         """
957         Stockage d'une valeur "value" pour le "step" dans la variable "name".
958         """
959         if name is None: raise ValueError("Storable object name is required for storage.")
960         if name not in self.__StoredObjects.keys():
961             raise ValueError("No such name '%s' exists in storable objects."%name)
962         self.__StoredObjects[name].store( value=value, **kwargs )
963
964     def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
965         """
966         Ajoute dans les objets stockables un nouvel objet défini par son nom,
967         son type de Persistence et son type de base à chaque pas.
968         """
969         if name is None: raise ValueError("Object name is required for adding an object.")
970         if name in self.__StoredObjects.keys():
971             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
972         if basetype is None:
973             self.__StoredObjects[name] = persistenceType( name=str(name) )
974         else:
975             self.__StoredObjects[name] = persistenceType( name=str(name), basetype=basetype )
976
977     def get_object(self, name=None ):
978         """
979         Renvoie l'objet de type Persistence qui porte le nom demandé.
980         """
981         if name is None: raise ValueError("Object name is required for retrieving an object.")
982         if name not in self.__StoredObjects.keys():
983             raise ValueError("No such name '%s' exists in stored objects."%name)
984         return self.__StoredObjects[name]
985
986     def set_object(self, name=None, objet=None ):
987         """
988         Affecte directement un 'objet' qui porte le nom 'name' demandé.
989         Attention, il n'est pas effectué de vérification sur le type, qui doit
990         comporter les méthodes habituelles de Persistence pour que cela
991         fonctionne.
992         """
993         if name is None: raise ValueError("Object name is required for setting an object.")
994         if name in self.__StoredObjects.keys():
995             raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
996         self.__StoredObjects[name] = objet
997
998     def del_object(self, name=None ):
999         """
1000         Supprime un objet de la liste des objets stockables.
1001         """
1002         if name is None: raise ValueError("Object name is required for retrieving an object.")
1003         if name not in self.__StoredObjects.keys():
1004             raise ValueError("No such name '%s' exists in stored objects."%name)
1005         del self.__StoredObjects[name]
1006
1007     # ---------------------------------------------------------
1008     # Méthodes d'accès de type dictionnaire
1009     def __getitem__(self, name=None ):
1010         "x.__getitem__(y) <==> x[y]"
1011         return self.get_object( name )
1012
1013     def __setitem__(self, name=None, objet=None ):
1014         "x.__setitem__(i, y) <==> x[i]=y"
1015         self.set_object( name, objet )
1016
1017     def keys(self):
1018         "D.keys() -> list of D's keys"
1019         return self.get_stored_objects(hideVoidObjects = False)
1020
1021     def values(self):
1022         "D.values() -> list of D's values"
1023         return self.__StoredObjects.values()
1024
1025     def items(self):
1026         "D.items() -> list of D's (key, value) pairs, as 2-tuples"
1027         return self.__StoredObjects.items()
1028
1029     # ---------------------------------------------------------
1030     def get_stored_objects(self, hideVoidObjects = False):
1031         "Renvoie la liste des objets présents"
1032         objs = self.__StoredObjects.keys()
1033         if hideVoidObjects:
1034             usedObjs = []
1035             for k in objs:
1036                 try:
1037                     if len(self.__StoredObjects[k]) > 0: usedObjs.append( k )
1038                 finally:
1039                     pass
1040             objs = usedObjs
1041         objs = sorted(objs)
1042         return objs
1043
1044     # ---------------------------------------------------------
1045     def save_composite(self, filename=None, mode="pickle", compress="gzip"):
1046         """
1047         Enregistre l'objet dans le fichier indiqué selon le "mode" demandé,
1048         et renvoi le nom du fichier
1049         """
1050         if filename is None:
1051             if compress == "gzip":
1052                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.gz"
1053             elif compress == "bzip2":
1054                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.bz2"
1055             else:
1056                 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl"
1057         else:
1058             filename = os.path.abspath( filename )
1059         #
1060         if mode == "pickle":
1061             if compress == "gzip":
1062                 output = gzip.open( filename, 'wb')
1063             elif compress == "bzip2":
1064                 output = bz2.BZ2File( filename, 'wb')
1065             else:
1066                 output = open( filename, 'wb')
1067             pickle.dump(self, output)
1068             output.close()
1069         else:
1070             raise ValueError("Save mode '%s' unknown. Choose another one."%mode)
1071         #
1072         return filename
1073
1074     def load_composite(self, filename=None, mode="pickle", compress="gzip"):
1075         """
1076         Recharge un objet composite sauvé en fichier
1077         """
1078         if filename is None:
1079             raise ValueError("A file name if requested to load a composite.")
1080         else:
1081             filename = os.path.abspath( filename )
1082         #
1083         if mode == "pickle":
1084             if compress == "gzip":
1085                 pkl_file = gzip.open( filename, 'rb')
1086             elif compress == "bzip2":
1087                 pkl_file = bz2.BZ2File( filename, 'rb')
1088             else:
1089                 pkl_file = open(filename, 'rb')
1090             output = pickle.load(pkl_file)
1091             for k in output.keys():
1092                 self[k] = output[k]
1093         else:
1094             raise ValueError("Load mode '%s' unknown. Choose another one."%mode)
1095         #
1096         return filename
1097
1098 # ==============================================================================
1099 if __name__ == "__main__":
1100     print("\n AUTODIAGNOSTIC\n")