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