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