1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2008-2024 EDF R&D
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.
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.
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
19 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
21 # Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
24 Définit des outils de persistance et d'enregistrement de séries de valeurs
25 pour analyse ultérieure ou utilisation de calcul.
27 __author__ = "Jean-Philippe ARGAUD"
30 import os, numpy, copy, math
31 import gzip, bz2, pickle
33 from daCore.PlatformInfo import PathManagement ; PathManagement() # noqa: E702,E203
34 from daCore.PlatformInfo import PlatformInfo
36 mfp = lpi.MaximumPrecision()
41 # ==============================================================================
42 class Persistence(object):
44 Classe générale de persistance définissant les accesseurs nécessaires
48 "__name", "__unit", "__basetype", "__values", "__tags", "__dynamic",
49 "__g", "__title", "__ltitle", "__pause", "__dataobservers",
52 def __init__(self, name="", unit="", basetype=str):
56 basetype : type de base de l'objet stocké à chaque pas
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
65 self.__name = str(name)
66 self.__unit = str(unit)
68 self.__basetype = basetype
73 self.__dynamic = False
79 self.__dataobservers = []
81 def basetype(self, basetype=None):
83 Renvoie ou met en place le type de base des objets stockés
86 return self.__basetype
88 self.__basetype = basetype
90 def store(self, value=None, **kwargs):
92 Stocke une valeur avec ses informations de filtrage.
95 raise ValueError("Value argument required")
97 self.__values.append(copy.copy(self.__basetype(value)))
98 self.__tags.append(kwargs)
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 )
108 if not isinstance(order, (list, tuple)):
110 if not isinstance(dovar, dict):
112 if not bool(osync): # Async observation
113 hook( self, parameters, order, dovar )
114 else: # Sync observations
116 if len(dovar[v]) != len(self):
119 hook( self, parameters, order, dovar )
121 def pop(self, item=None):
123 Retire une valeur enregistrée par son index de stockage. Sans argument,
124 retire le dernier objet enregistre.
128 self.__values.pop(__index)
129 self.__tags.pop(__index)
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.
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]:
146 elif self.__basetype in [list, dict]:
147 return (len(self.__values[-1]),)
151 raise ValueError("Object has no shape before its first storage")
153 # ---------------------------------------------------------
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])
162 "x.__len__() <==> len(x)"
163 return len(self.__values)
168 def __getitem__(self, index=None ):
169 "x.__getitem__(y) <==> x[y]"
170 return copy.copy(self.__values[index])
172 def count(self, value):
173 "L.count(value) -> integer -- return number of occurrences of value"
174 return self.__values.count(value)
176 def index(self, value, start=0, stop=None):
177 "L.index(value, [start, [stop]]) -> integer -- return first index of value."
179 stop = len(self.__values)
180 return self.__values.index(value, start, stop)
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:
190 for i in __indexOfFilteredItems:
191 if tagKey in self.__tags[i]:
192 if self.__tags[i][tagKey] == kwargs[tagKey]:
194 elif isinstance(kwargs[tagKey], (list, tuple)) and self.__tags[i][tagKey] in kwargs[tagKey]:
196 __indexOfFilteredItems = __tmp
197 if len(__indexOfFilteredItems) == 0:
199 return __indexOfFilteredItems
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]
207 def keys(self, keyword=None, **kwargs):
208 "D.keys() -> list of D's keys"
209 __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
211 for i in __indexOfFilteredItems:
212 if keyword in self.__tags[i]:
213 __keys.append( self.__tags[i][keyword] )
215 __keys.append( None )
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)
222 for i in __indexOfFilteredItems:
223 if keyword in self.__tags[i]:
224 __pairs.append( (self.__tags[i][keyword], self.__values[i]) )
226 __pairs.append( (None, self.__values[i]) )
230 "D.tagkeys() -> list of D's tag keys"
232 for dicotags in self.__tags:
233 __allKeys.extend( list(dicotags.keys()) )
234 __allKeys = sorted(set(__allKeys))
237 # def valueserie(self, item=None, allSteps=True, **kwargs):
238 # if item is not None:
239 # return self.__values[item]
241 # __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
242 # if not allSteps and len(__indexOfFilteredItems) > 0:
243 # return self.__values[__indexOfFilteredItems[0]]
245 # return [self.__values[i] for i in __indexOfFilteredItems]
247 def tagserie(self, item=None, withValues=False, outputTag=None, **kwargs):
248 "D.tagserie() -> list of D's tag serie"
250 __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
252 __indexOfFilteredItems = [item,]
254 # Dans le cas où la sortie donne les valeurs d'un "outputTag"
255 if outputTag is not None and isinstance(outputTag, str):
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))
263 # Dans le cas où la sortie donne les tags satisfaisants aux conditions
266 return [self.__tags[index] for index in __indexOfFilteredItems]
269 for index in __indexOfFilteredItems:
270 allTags.update( self.__tags[index] )
271 allKeys = sorted(allTags.keys())
274 # ---------------------------------------------------------
276 def stepnumber(self):
278 return len(self.__values)
281 def stepserie(self, **kwargs):
282 "Nombre de pas filtrés"
283 __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
284 return __indexOfFilteredItems
287 def steplist(self, **kwargs):
288 "Nombre de pas filtrés"
289 __indexOfFilteredItems = self.__filteredIndexes(**kwargs)
290 return list(__indexOfFilteredItems)
292 # ---------------------------------------------------------
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
300 return [numpy.mean(item, dtype=mfp).astype('float') for item in self.__values]
302 raise TypeError("Base type is incompatible with numpy")
304 def stds(self, ddof=0):
306 Renvoie la série contenant, à chaque pas, l'écart-type des données
307 au pas. Il faut que le type de base soit compatible avec les types
310 ddof : c'est le nombre de degrés de liberté pour le calcul de
311 l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
314 if numpy.version.version >= '1.1.0':
315 return [numpy.array(item).std(ddof=ddof, dtype=mfp).astype('float') for item in self.__values]
317 return [numpy.array(item).std(dtype=mfp).astype('float') for item in self.__values]
319 raise TypeError("Base type is incompatible with numpy")
323 Renvoie la série contenant, à chaque pas, la somme des données au pas.
324 Il faut que le type de base soit compatible avec les types élémentaires
328 return [numpy.array(item).sum() for item in self.__values]
330 raise TypeError("Base type is incompatible with numpy")
334 Renvoie la série contenant, à chaque pas, le minimum des données au pas.
335 Il faut que le type de base soit compatible avec les types élémentaires
339 return [numpy.array(item).min() for item in self.__values]
341 raise TypeError("Base type is incompatible with numpy")
345 Renvoie la série contenant, à chaque pas, la maximum des données au pas.
346 Il faut que le type de base soit compatible avec les types élémentaires
350 return [numpy.array(item).max() for item in self.__values]
352 raise TypeError("Base type is incompatible with numpy")
354 def powers(self, x2):
356 Renvoie la série contenant, à chaque pas, la puissance "**x2" au pas.
357 Il faut que le type de base soit compatible avec les types élémentaires
361 return [numpy.power(item, x2) for item in self.__values]
363 raise TypeError("Base type is incompatible with numpy")
365 def norms(self, _ord=None):
367 Norm (_ord : voir numpy.linalg.norm)
369 Renvoie la série contenant, à chaque pas, la norme des données au pas.
370 Il faut que le type de base soit compatible avec les types élémentaires
374 return [numpy.linalg.norm(item, _ord) for item in self.__values]
376 raise TypeError("Base type is incompatible with numpy")
378 def traces(self, offset=0):
382 Renvoie la série contenant, à chaque pas, la trace (avec l'offset) des
383 données au pas. Il faut que le type de base soit compatible avec les
384 types élémentaires numpy.
387 return [numpy.trace(item, offset, dtype=mfp) for item in self.__values]
389 raise TypeError("Base type is incompatible with numpy")
391 def maes(self, _predictor=None):
393 Mean Absolute Error (MAE)
394 mae(dX) = 1/n sum(dX_i)
396 Renvoie la série contenant, à chaque pas, la MAE des données au pas.
397 Il faut que le type de base soit compatible avec les types élémentaires
398 numpy. C'est réservé aux variables d'écarts ou d'incréments si le
399 prédicteur est None, sinon c'est appliqué à l'écart entre les données
400 au pas et le prédicteur au même pas.
402 if _predictor is None:
404 return [numpy.mean(numpy.abs(item)) for item in self.__values]
406 raise TypeError("Base type is incompatible with numpy")
408 if len(_predictor) != len(self.__values):
409 raise ValueError("Predictor number of steps is incompatible with the values")
410 for i, item in enumerate(self.__values):
411 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
412 raise ValueError("Predictor size at step %i is incompatible with the values"%i)
414 return [numpy.mean(numpy.abs(numpy.ravel(item) - numpy.ravel(_predictor[i]))) for i, item in enumerate(self.__values)]
416 raise TypeError("Base type is incompatible with numpy")
418 def mses(self, _predictor=None):
420 Mean-Square Error (MSE) ou Mean-Square Deviation (MSD)
421 mse(dX) = 1/n sum(dX_i**2)
423 Renvoie la série contenant, à chaque pas, la MSE des données au pas. Il
424 faut que le type de base soit compatible avec les types élémentaires
425 numpy. C'est réservé aux variables d'écarts ou d'incréments si le
426 prédicteur est None, sinon c'est appliqué à l'écart entre les données
427 au pas et le prédicteur au même pas.
429 if _predictor is None:
431 __n = self.shape()[0]
432 return [(numpy.linalg.norm(item)**2 / __n) for item in self.__values]
434 raise TypeError("Base type is incompatible with numpy")
436 if len(_predictor) != len(self.__values):
437 raise ValueError("Predictor number of steps is incompatible with the values")
438 for i, item in enumerate(self.__values):
439 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
440 raise ValueError("Predictor size at step %i is incompatible with the values"%i)
442 __n = self.shape()[0]
443 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i]))**2 / __n) for i, item in enumerate(self.__values)]
445 raise TypeError("Base type is incompatible with numpy")
447 msds = mses # Mean-Square Deviation (MSD=MSE)
449 def rmses(self, _predictor=None):
451 Root-Mean-Square Error (RMSE) ou Root-Mean-Square Deviation (RMSD)
452 rmse(dX) = sqrt( 1/n sum(dX_i**2) ) = sqrt( mse(dX) )
454 Renvoie la série contenant, à chaque pas, la RMSE des données au pas.
455 Il faut que le type de base soit compatible avec les types élémentaires
456 numpy. C'est réservé aux variables d'écarts ou d'incréments si le
457 prédicteur est None, sinon c'est appliqué à l'écart entre les données
458 au pas et le prédicteur au même pas.
460 if _predictor is None:
462 __n = self.shape()[0]
463 return [(numpy.linalg.norm(item) / math.sqrt(__n)) for item in self.__values]
465 raise TypeError("Base type is incompatible with numpy")
467 if len(_predictor) != len(self.__values):
468 raise ValueError("Predictor number of steps is incompatible with the values")
469 for i, item in enumerate(self.__values):
470 if numpy.asarray(_predictor[i]).size != numpy.asarray(item).size:
471 raise ValueError("Predictor size at step %i is incompatible with the values"%i)
473 __n = self.shape()[0]
474 return [(numpy.linalg.norm(numpy.ravel(item) - numpy.ravel(_predictor[i])) / math.sqrt(__n)) for i, item in enumerate(self.__values)]
476 raise TypeError("Base type is incompatible with numpy")
478 rmsds = rmses # Root-Mean-Square Deviation (RMSD=RMSE)
485 geometry = "600x400",
488 "Préparation des plots"
490 # Vérification de la disponibilité du module Gnuplot
491 if not lpi.has_gnuplot:
492 raise ImportError("The Gnuplot module is required to plot the object.")
494 # Vérification et compléments sur les paramètres d'entrée
497 __geometry = str(geometry)
498 __sizespec = (__geometry.split('+')[0]).replace('x', ',')
501 Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
503 self.__g = Gnuplot.Gnuplot() # persist=1
504 self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
505 self.__g('set style data lines')
507 self.__g('set autoscale')
508 self.__g('set xlabel "' + str(xlabel) + '"')
509 self.__g('set ylabel "' + str(ylabel) + '"')
511 self.__ltitle = ltitle
522 geometry = "600x400",
528 Renvoie un affichage de la valeur à chaque pas, si elle est compatible
529 avec un affichage Gnuplot (donc essentiellement un vecteur). Si
530 l'argument "step" existe dans la liste des pas de stockage effectués,
531 renvoie l'affichage de la valeur stockée à ce pas "step". Si l'argument
532 "item" est correct, renvoie l'affichage de la valeur stockée au numéro
533 "item". Par défaut ou en l'absence de "step" ou "item", renvoie un
534 affichage successif de tous les pas.
537 - step : valeur du pas à afficher
538 - item : index de la valeur à afficher
539 - steps : liste unique des pas de l'axe des X, ou None si c'est
540 la numérotation par défaut
541 - title : base du titre général, qui sera automatiquement
542 complétée par la mention du pas
543 - xlabel : label de l'axe des X
544 - ylabel : label de l'axe des Y
545 - ltitle : titre associé au vecteur tracé
546 - geometry : taille en pixels de la fenêtre et position du coin haut
547 gauche, au format X11 : LxH+X+Y (défaut : 600x400)
548 - filename : base de nom de fichier Postscript pour une sauvegarde,
549 qui est automatiquement complétée par le numéro du
550 fichier calculé par incrément simple de compteur
551 - dynamic : effectue un affichage des valeurs à chaque stockage
552 (au-delà du second). La méthode "plots" permet de
553 déclarer l'affichage dynamique, et c'est la méthode
554 "__replots" qui est utilisée pour l'effectuer
555 - persist : booléen indiquant que la fenêtre affichée sera
556 conservée lors du passage au dessin suivant
557 Par défaut, persist = False
558 - pause : booléen indiquant une pause après chaque tracé, et
560 Par défaut, pause = True
562 if not self.__dynamic:
563 self.__preplots(title, xlabel, ylabel, ltitle, geometry, persist, pause )
565 self.__dynamic = True
566 if len(self.__values) == 0:
569 # Tracé du ou des vecteurs demandés
571 if step is not None and step < len(self.__values):
573 elif item is not None and item < len(self.__values):
576 indexes = indexes + list(range(len(self.__values)))
579 for index in indexes:
580 self.__g('set title "' + str(title) + ' (pas ' + str(index) + ')"')
581 if isinstance(steps, (list, numpy.ndarray)):
584 Steps = list(range(len(self.__values[index])))
586 self.__g.plot( Gnuplot.Data( Steps, self.__values[index], title=ltitle ) )
590 stepfilename = "%s_%03i.ps"%(filename, i)
591 if os.path.isfile(stepfilename):
592 raise ValueError("Error: a file with this name \"%s\" already exists."%stepfilename)
593 self.__g.hardcopy(filename=stepfilename, color=1)
595 eval(input('Please press return to continue...\n'))
599 Affichage dans le cas du suivi dynamique de la variable
601 if self.__dynamic and len(self.__values) < 2:
604 self.__g('set title "' + str(self.__title))
605 Steps = list(range(len(self.__values)))
606 self.__g.plot( Gnuplot.Data( Steps, self.__values, title=self.__ltitle ) )
609 eval(input('Please press return to continue...\n'))
611 # ---------------------------------------------------------
612 # On pourrait aussi utiliser d'autres attributs d'un "array" comme "tofile"
615 Renvoie la moyenne sur toutes les valeurs sans tenir compte de la
616 longueur des pas. Il faut que le type de base soit compatible avec
617 les types élémentaires numpy.
620 return numpy.mean(self.__values, axis=0, dtype=mfp).astype('float')
622 raise TypeError("Base type is incompatible with numpy")
624 def std(self, ddof=0):
626 Renvoie l'écart-type de toutes les valeurs sans tenir compte de la
627 longueur des pas. Il faut que le type de base soit compatible avec
628 les types élémentaires numpy.
630 ddof : c'est le nombre de degrés de liberté pour le calcul de
631 l'écart-type, qui est dans le diviseur. Inutile avant Numpy 1.1
634 if numpy.version.version >= '1.1.0':
635 return numpy.asarray(self.__values).std(ddof=ddof, axis=0).astype('float')
637 return numpy.asarray(self.__values).std(axis=0).astype('float')
639 raise TypeError("Base type is incompatible with numpy")
643 Renvoie la somme de toutes les valeurs sans tenir compte de la
644 longueur des pas. Il faut que le type de base soit compatible avec
645 les types élémentaires numpy.
648 return numpy.asarray(self.__values).sum(axis=0)
650 raise TypeError("Base type is incompatible with numpy")
654 Renvoie le minimum de toutes les valeurs sans tenir compte de la
655 longueur des pas. Il faut que le type de base soit compatible avec
656 les types élémentaires numpy.
659 return numpy.asarray(self.__values).min(axis=0)
661 raise TypeError("Base type is incompatible with numpy")
665 Renvoie le maximum de toutes les valeurs sans tenir compte de la
666 longueur des pas. Il faut que le type de base soit compatible avec
667 les types élémentaires numpy.
670 return numpy.asarray(self.__values).max(axis=0)
672 raise TypeError("Base type is incompatible with numpy")
676 Renvoie la somme cumulée de toutes les valeurs sans tenir compte de la
677 longueur des pas. Il faut que le type de base soit compatible avec
678 les types élémentaires numpy.
681 return numpy.asarray(self.__values).cumsum(axis=0)
683 raise TypeError("Base type is incompatible with numpy")
691 geometry = "600x400",
696 Renvoie un affichage unique pour l'ensemble des valeurs à chaque pas, si
697 elles sont compatibles avec un affichage Gnuplot (donc essentiellement
698 un vecteur). Si l'argument "step" existe dans la liste des pas de
699 stockage effectués, renvoie l'affichage de la valeur stockée à ce pas
700 "step". Si l'argument "item" est correct, renvoie l'affichage de la
701 valeur stockée au numéro "item".
704 - steps : liste unique des pas de l'axe des X, ou None si c'est
705 la numérotation par défaut
706 - title : base du titre général, qui sera automatiquement
707 complétée par la mention du pas
708 - xlabel : label de l'axe des X
709 - ylabel : label de l'axe des Y
710 - ltitle : titre associé au vecteur tracé
711 - geometry : taille en pixels de la fenêtre et position du coin haut
712 gauche, au format X11 : LxH+X+Y (défaut : 600x400)
713 - filename : nom de fichier Postscript pour une sauvegarde
714 - persist : booléen indiquant que la fenêtre affichée sera
715 conservée lors du passage au dessin suivant
716 Par défaut, persist = False
717 - pause : booléen indiquant une pause après chaque tracé, et
719 Par défaut, pause = True
722 # Vérification de la disponibilité du module Gnuplot
723 if not lpi.has_gnuplot:
724 raise ImportError("The Gnuplot module is required to plot the object.")
726 # Vérification et compléments sur les paramètres d'entrée
729 if isinstance(steps, (list, numpy.ndarray)):
732 Steps = list(range(len(self.__values[0])))
733 __geometry = str(geometry)
734 __sizespec = (__geometry.split('+')[0]).replace('x', ',')
737 Gnuplot.GnuplotOpts.gnuplot_command = 'gnuplot -persist '
739 self.__g = Gnuplot.Gnuplot() # persist=1
740 self.__g('set terminal ' + Gnuplot.GnuplotOpts.default_term + ' size ' + __sizespec)
741 self.__g('set style data lines')
743 self.__g('set autoscale')
744 self.__g('set title "' + str(title) + '"')
745 self.__g('set xlabel "' + str(xlabel) + '"')
746 self.__g('set ylabel "' + str(ylabel) + '"')
748 # Tracé du ou des vecteurs demandés
749 indexes = list(range(len(self.__values)))
750 self.__g.plot( Gnuplot.Data( Steps, self.__values[indexes.pop(0)], title=ltitle + " (pas 0)" ) )
751 for index in indexes:
752 self.__g.replot( Gnuplot.Data( Steps, self.__values[index], title=ltitle + " (pas %i)"%index ) )
755 self.__g.hardcopy(filename=filename, color=1)
757 eval(input('Please press return to continue...\n'))
759 # ---------------------------------------------------------
762 Renvoie la série sous la forme d'une unique matrice avec les données au
763 pas rangées par ligne
766 return numpy.asarray(self.__values)
768 raise TypeError("Base type is incompatible with numpy")
772 Renvoie la série sous la forme d'une unique matrice avec les données au
773 pas rangées par colonne
776 return numpy.asarray(self.__values).transpose()
777 # Eqvlt: return numpy.stack([numpy.ravel(sv) for sv in self.__values], axis=1)
779 raise TypeError("Base type is incompatible with numpy")
781 # ---------------------------------------------------------
782 def setDataObserver(self, HookFunction = None, HookParameters = None, Scheduler = None, Order = None, OSync = True, DOVar = None):
784 Association à la variable d'un triplet définissant un observer.
786 Les variables Order et DOVar sont utilisées pour un observer
787 multi-variable. Le Scheduler attendu est une fréquence, une simple
788 liste d'index ou un range des index.
791 # Vérification du Scheduler
792 # -------------------------
794 if isinstance(Scheduler, int): # Considéré comme une fréquence à partir de 0
795 Schedulers = range( 0, maxiter, int(Scheduler) )
796 elif isinstance(Scheduler, range): # Considéré comme un itérateur
797 Schedulers = Scheduler
798 elif isinstance(Scheduler, (list, tuple)): # Considéré comme des index explicites
799 Schedulers = [int(i) for i in Scheduler] # Similaire à map( int, Scheduler ) # noqa: E262
800 else: # Dans tous les autres cas, activé par défaut
801 Schedulers = range( 0, maxiter )
803 # Stockage interne de l'observer dans la variable
804 # -----------------------------------------------
805 self.__dataobservers.append( [HookFunction, HookParameters, Schedulers, Order, OSync, DOVar] )
807 def removeDataObserver(self, HookFunction = None, AllObservers = False):
809 Suppression d'un observer nommé sur la variable.
811 On peut donner dans HookFunction la même fonction que lors de la
812 définition, ou un simple string qui est le nom de la fonction. Si
813 AllObservers est vrai, supprime tous les observers enregistrés.
815 if hasattr(HookFunction, "func_name"):
816 name = str( HookFunction.func_name )
817 elif hasattr(HookFunction, "__name__"):
818 name = str( HookFunction.__name__ )
819 elif isinstance(HookFunction, str):
820 name = str( HookFunction )
826 for [hf, _, _, _, _, _] in self.__dataobservers:
828 if name is hf.__name__ or AllObservers:
829 index_to_remove.append( ih )
830 index_to_remove.reverse()
831 for ih in index_to_remove:
832 self.__dataobservers.pop( ih )
833 return len(index_to_remove)
835 def hasDataObserver(self):
836 return bool(len(self.__dataobservers) > 0)
838 # ==============================================================================
839 class SchedulerTrigger(object):
841 Classe générale d'interface de type Scheduler/Trigger
846 simplifiedCombo = None,
848 endTime = int( 1e9 ),
854 # ==============================================================================
855 class OneScalar(Persistence):
857 Classe définissant le stockage d'une valeur unique réelle (float) par pas.
859 Le type de base peut être changé par la méthode "basetype", mais il faut que
860 le nouveau type de base soit compatible avec les types par éléments de
861 numpy. On peut même utiliser cette classe pour stocker des vecteurs/listes
862 ou des matrices comme dans les classes suivantes, mais c'est déconseillé
863 pour conserver une signification claire des noms.
867 def __init__(self, name="", unit="", basetype = float):
868 Persistence.__init__(self, name, unit, basetype)
870 class OneIndex(Persistence):
872 Classe définissant le stockage d'une valeur unique entière (int) par pas.
876 def __init__(self, name="", unit="", basetype = int):
877 Persistence.__init__(self, name, unit, basetype)
879 class OneVector(Persistence):
881 Classe de stockage d'une liste de valeurs numériques homogènes par pas. Ne
882 pas utiliser cette classe pour des données hétérogènes, mais "OneList".
886 def __init__(self, name="", unit="", basetype = numpy.ravel):
887 Persistence.__init__(self, name, unit, basetype)
889 class OneMatrice(Persistence):
891 Classe de stockage d'une matrice de valeurs homogènes par pas.
895 def __init__(self, name="", unit="", basetype = numpy.array):
896 Persistence.__init__(self, name, unit, basetype)
898 class OneMatrix(Persistence):
900 Classe de stockage d'une matrice de valeurs homogènes par pas.
904 def __init__(self, name="", unit="", basetype = numpy.matrix):
905 Persistence.__init__(self, name, unit, basetype)
907 class OneList(Persistence):
909 Classe de stockage d'une liste de valeurs hétérogènes (list) par pas. Ne
910 pas utiliser cette classe pour des données numériques homogènes, mais
915 def __init__(self, name="", unit="", basetype = list):
916 Persistence.__init__(self, name, unit, basetype)
919 "Fonction transparente, sans effet sur son argument"
922 class OneNoType(Persistence):
924 Classe de stockage d'un objet sans modification (cast) de type. Attention,
925 selon le véritable type de l'objet stocké à chaque pas, les opérations
926 arithmétiques à base de numpy peuvent être invalides ou donner des
927 résultats inattendus. Cette classe n'est donc à utiliser qu'à bon escient
928 volontairement, et pas du tout par défaut.
932 def __init__(self, name="", unit="", basetype = NoType):
933 Persistence.__init__(self, name, unit, basetype)
935 # ==============================================================================
936 class CompositePersistence(object):
938 Structure de stockage permettant de rassembler plusieurs objets de
941 Des objets par défaut sont prévus, et des objets supplémentaires peuvent
944 __slots__ = ("__name", "__StoredObjects")
946 def __init__(self, name="", defaults=True):
950 La gestion interne des données est exclusivement basée sur les
951 variables initialisées ici (qui ne sont pas accessibles depuis
952 l'extérieur des objets comme des attributs) :
953 __StoredObjects : objets de type persistence collectés dans cet objet
955 self.__name = str(name)
957 self.__StoredObjects = {}
959 # Definition des objets par defaut
960 # --------------------------------
962 self.__StoredObjects["Informations"] = OneNoType("Informations")
963 self.__StoredObjects["Background"] = OneVector("Background", basetype=numpy.array)
964 self.__StoredObjects["BackgroundError"] = OneMatrix("BackgroundError")
965 self.__StoredObjects["Observation"] = OneVector("Observation", basetype=numpy.array)
966 self.__StoredObjects["ObservationError"] = OneMatrix("ObservationError")
967 self.__StoredObjects["Analysis"] = OneVector("Analysis", basetype=numpy.array)
968 self.__StoredObjects["AnalysisError"] = OneMatrix("AnalysisError")
969 self.__StoredObjects["Innovation"] = OneVector("Innovation", basetype=numpy.array)
970 self.__StoredObjects["KalmanGainK"] = OneMatrix("KalmanGainK")
971 self.__StoredObjects["OperatorH"] = OneMatrix("OperatorH")
972 self.__StoredObjects["RmsOMA"] = OneScalar("RmsOMA")
973 self.__StoredObjects["RmsOMB"] = OneScalar("RmsOMB")
974 self.__StoredObjects["RmsBMA"] = OneScalar("RmsBMA")
977 def store(self, name=None, value=None, **kwargs):
979 Stockage d'une valeur "value" pour le "step" dans la variable "name".
982 raise ValueError("Storable object name is required for storage.")
983 if name not in self.__StoredObjects.keys():
984 raise ValueError("No such name '%s' exists in storable objects."%name)
985 self.__StoredObjects[name].store( value=value, **kwargs )
987 def add_object(self, name=None, persistenceType=Persistence, basetype=None ):
989 Ajoute dans les objets stockables un nouvel objet défini par son nom,
990 son type de Persistence et son type de base à chaque pas.
993 raise ValueError("Object name is required for adding an object.")
994 if name in self.__StoredObjects.keys():
995 raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
997 self.__StoredObjects[name] = persistenceType( name=str(name) )
999 self.__StoredObjects[name] = persistenceType( name=str(name), basetype=basetype )
1001 def get_object(self, name=None ):
1003 Renvoie l'objet de type Persistence qui porte le nom demandé.
1006 raise ValueError("Object name is required for retrieving an object.")
1007 if name not in self.__StoredObjects.keys():
1008 raise ValueError("No such name '%s' exists in stored objects."%name)
1009 return self.__StoredObjects[name]
1011 def set_object(self, name=None, objet=None ):
1013 Affecte directement un 'objet' qui porte le nom 'name' demandé.
1014 Attention, il n'est pas effectué de vérification sur le type, qui doit
1015 comporter les méthodes habituelles de Persistence pour que cela
1019 raise ValueError("Object name is required for setting an object.")
1020 if name in self.__StoredObjects.keys():
1021 raise ValueError("An object with the same name '%s' already exists in storable objects. Choose another one."%name)
1022 self.__StoredObjects[name] = objet
1024 def del_object(self, name=None ):
1026 Supprime un objet de la liste des objets stockables.
1029 raise ValueError("Object name is required for retrieving an object.")
1030 if name not in self.__StoredObjects.keys():
1031 raise ValueError("No such name '%s' exists in stored objects."%name)
1032 del self.__StoredObjects[name]
1034 # ---------------------------------------------------------
1035 # Méthodes d'accès de type dictionnaire
1036 def __getitem__(self, name=None ):
1037 "x.__getitem__(y) <==> x[y]"
1038 return self.get_object( name )
1040 def __setitem__(self, name=None, objet=None ):
1041 "x.__setitem__(i, y) <==> x[i]=y"
1042 self.set_object( name, objet )
1045 "D.keys() -> list of D's keys"
1046 return self.get_stored_objects(hideVoidObjects = False)
1049 "D.values() -> list of D's values"
1050 return self.__StoredObjects.values()
1053 "D.items() -> list of D's (key, value) pairs, as 2-tuples"
1054 return self.__StoredObjects.items()
1056 # ---------------------------------------------------------
1057 def get_stored_objects(self, hideVoidObjects = False):
1058 "Renvoie la liste des objets présents"
1059 objs = self.__StoredObjects.keys()
1064 if len(self.__StoredObjects[k]) > 0:
1065 usedObjs.append( k )
1072 # ---------------------------------------------------------
1073 def save_composite(self, filename=None, mode="pickle", compress="gzip"):
1075 Enregistre l'objet dans le fichier indiqué selon le "mode" demandé,
1076 et renvoi le nom du fichier
1078 if filename is None:
1079 if compress == "gzip":
1080 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.gz"
1081 elif compress == "bzip2":
1082 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl.bz2"
1084 filename = os.tempnam( os.getcwd(), 'dacp' ) + ".pkl"
1086 filename = os.path.abspath( filename )
1088 if mode == "pickle":
1089 if compress == "gzip":
1090 output = gzip.open( filename, 'wb')
1091 elif compress == "bzip2":
1092 output = bz2.BZ2File( filename, 'wb')
1094 output = open( filename, 'wb')
1095 pickle.dump(self, output)
1098 raise ValueError("Save mode '%s' unknown. Choose another one."%mode)
1102 def load_composite(self, filename=None, mode="pickle", compress="gzip"):
1104 Recharge un objet composite sauvé en fichier
1106 if filename is None:
1107 raise ValueError("A file name if requested to load a composite.")
1109 filename = os.path.abspath( filename )
1111 if mode == "pickle":
1112 if compress == "gzip":
1113 pkl_file = gzip.open( filename, 'rb')
1114 elif compress == "bzip2":
1115 pkl_file = bz2.BZ2File( filename, 'rb')
1117 pkl_file = open(filename, 'rb')
1118 output = pickle.load(pkl_file)
1119 for k in output.keys():
1122 raise ValueError("Load mode '%s' unknown. Choose another one."%mode)
1126 # ==============================================================================
1127 if __name__ == "__main__":
1128 print("\n AUTODIAGNOSTIC\n")