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