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