From 8dda4ed6a341afdbf7697a822c033392d9edf022 Mon Sep 17 00:00:00 2001 From: Poulard Christine <christine.poulard@irstea.fr> Date: Sat, 8 May 2021 23:47:37 +0000 Subject: [PATCH] Upload New File, Atelier G --- GenerateurCruesMaxAnnuelles.py | 378 +++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 GenerateurCruesMaxAnnuelles.py diff --git a/GenerateurCruesMaxAnnuelles.py b/GenerateurCruesMaxAnnuelles.py new file mode 100644 index 0000000..8c95379 --- /dev/null +++ b/GenerateurCruesMaxAnnuelles.py @@ -0,0 +1,378 @@ +""" +CPoulard ; mai 2021 +côté "métier" : + montrer le principe des formules Tchégodaiev / Hazen utilisées en hydrologie (TD 1ere année ENTPE) et la relation fréquence/période de retour + démo du caractère aléatoire des crues : on constitue progressivement une série d' "observations" issues d'une loi de Gumbel +coté "Python" : mettre en oeuvre deux widgets de matplotlib (Checkbutton et Slider), faciles d'emploi mais au rendu pas très abouti ; définir les fonctions associées à des événements sur ces boutons ; manipuler les fonctions en tant qu'objet (fonction_en_cours = une des 3 fonctions disponibles...) +""" + +from matplotlib import pyplot as plt +import matplotlib.gridspec as gridspec +from matplotlib.widgets import Slider, CheckButtons, Button # widgets du module matplotlib ! +import numpy as np +from numpy.random import gumbel + + +# FONCTIONS +# +# trois fonctions correspondant chacune à une manière de définir un échantillon de nb valeurs +def echantillon_uniforme(nb): + return sorted(range(1, nb+1), reverse=True) + +def echantillon_constant(nb): + # non encore testé + return [1]*nb + +def echantillon_gumbel(nb, x0=196, gr=102): + # non encore testé + + return gumbel(loc=x0, scale=gr, size=nb) + +class SerieGumbel: + T_Gumbel_plot = [1.1, 2, 5, 10, 20, 50, 100] + + def __init__(self, gr, x0): + self.gr = gr + self.x0 = x0 + self.serie_chrono = [] + self.serie_triee = [] + self.serie_couleurs = [] + + self.QT10 = self.QdeT(10) + self.QT50 = self.QdeT(50) + self.QT100 = self.QdeT(100) + self.QGumbel_plot = [self.QdeT(T) for T in SerieGumbel.T_Gumbel_plot] + self.gr_echantillon = None + self.x0_echantillon = None + print("quantiles ", self.QT10, self.QT50, self.QT100) + + def attribution_couleur(self, Q): + + if Q < self.QT10: + couleur = "black" + elif Q < self.QT50: + couleur = "green" + elif Q < self.QT100: + couleur = "blue" + else: + couleur = 'red' + return couleur + + def QdeT(self, T): + """Donne la valeur estimée pour une période de retour donnée selon une loi de Gumbel + Args: + T (float) : période de retour + gr, x0 (float) : paramètres d'échelle et de position de la loi de Gumbel + Returns : + QdeT(float) : valeur estimée + """ + Fnondepassement = 1 - (1 / T) # fréquence au non dépassement = 1 - f + u = -np.log(-np.log(Fnondepassement)) # log x s'obtient par np.log(x) + Q = self.x0 + self.gr * u + return Q + + def TdeQ(self, Q): + """Donne la valeur estimée pour une période de retour donnée selon une loi de Gumbel + Args: + Q (float) : valeur + gr, x0 (float) : paramètres d'échelle et de position de la loi de Gumbel + Returns : + TdeQ(float) : période de retour estimée + """ + u = (Q - self.x0) / self.gr # log x s'obtient par math.log(x) + Fnondepassement = np.exp(-1 * np.exp(-u)) + + T = 1 / (1 - Fnondepassement) # fréquence au non dépassement = 1 - f + + return T + + def ajout_obs(self, nb): + nouvelles_obs = gumbel(loc=self.x0, scale=self.gr, size=nb) + self.serie_chrono.extend(nouvelles_obs) + self.serie_couleurs.extend([self.attribution_couleur(Q) for Q in nouvelles_obs]) + self.serie_triee.extend(nouvelles_obs) + self.serie_triee.sort(reverse=True) + + self.gr_echantillon = 0.78 * np.std(self.serie_chrono) # gradex = paramètre d'échelle + self.x0_echantillon = np.mean(self.serie_chrono) - 0.577 * self.gr_echantillon # "ordonnée à l'origine" = paramètre de position + + +# Loi de Gumbel (max annuels) +#définition des lois d'ajustement +def AjustementParametresDeGumbel(moyenne, ecart_type): # 2 arguments à calculer par ailleurs + """Ajuste une loi de Gumbel, usuelle en hydrologie pour ajuster des valeurs max annuelles + à partir "seulement" de la moyenne et l'écart-type de l'échantillon + Elle renvoie ses 2 paramètres gr et x0 calculés par la méthode des moments" + # ils sont donc moins sensibles aux points exceptionnels que la régression linéaire (distances au CARRE) + Args: + moyenne (float) : moyenne de l'échantillon + ecart_type (float) : ecart-type de l'échantillon + Returns : + gr(float) : paramètre d'échelle, gradex + x0(float) : paramètre de position + + """ + + gr = 0.78 * ecart_type # gradex = paramètre d'échelle + x0 = moyenne - 0.577 * gr # "ordonnée à l'origine" = paramètre de position + + return gr, x0 + + +def QdeT_Gumbel(T, gr, x0): + """Donne la valeur estimée pour une période de retour donnée selon une loi de Gumbel + Args: + T (float) : période de retour + gr, x0 (float) : paramètres d'échelle et de position de la loi de Gumbel + Returns : + QdeT(float) : valeur estimée + """ + Fnondepassement = 1 - (1 / T) # fréquence au non dépassement = 1 - f + u = -np.log(-np.log(Fnondepassement)) # log x s'obtient par np.log(x) + + QdeT = x0 + gr * u + + return QdeT + +def TdeQ_Gumbel(Q, gr, x0): + """Donne la valeur estimée pour une période de retour donnée selon une loi de Gumbel + Args: + Q (float) : valeur + gr, x0 (float) : paramètres d'échelle et de position de la loi de Gumbel + Returns : + TdeQ(float) : période de retour estimée + """ + u = (Q - x0) / gr # log x s'obtient par math.log(x) + Fnondepassement = np.exp(-1 * np.exp(-u)) + + TdeQ = 1 / (1 - Fnondepassement) # fréquence au non dépassement = 1 - f + + return TdeQ + + +# fonction paramétrée calculant la fréquence par une fonction paramétrée par a et b +# puis 2 fonctions calculant la période de retour par la même fonction paramétrée et par une fonction avec valeurs fixes +# EVIDEMMENT IL Y A REDONDANCE pour l'exemple ; la première fonction suffirait ! + +def freq_emp_parametre(n_annees, a, b): + # pour la formule de Chegodayev, a = 0.3 et b=0.4 + + plotting_positions_freq = [ 1 - (((indice + 1) - a) / ((n_annees + b) )) for indice in range(n_annees)] + # au lieu d'envoyer une liste brute pour l'affichage, + # on crée une liste de freq formattées (:.2f = flottants 2 décimales) séparées par des "/" + print("Fréquences empiriques : ", " / ".join([f"{freq:.2f}" for freq in plotting_positions_freq])) + return plotting_positions_freq + +def T_emp_parametre(n_annees, a , b): + # T=1/freq de la fonction freq_emp_parametre, cas général de la précédente + plotting_positions_param = [((n_annees + b) / ((indice + 1) - a)) for indice in range(n_annees)] + print(f"Périodes de retour empiriques, a={a:.1f}, b={b:.1f}, N={n_annees} : ", " / ".join([f"{freq:.2f}" for freq in plotting_positions_param])) + return plotting_positions_param + + +def update_vlines(courbe_en_vlines, x, ymin=None, ymax=None): + # mise à jour de courbes de type vlines + # https://stackoverflow.com/questions/29331401/updating-pyplot-vlines-in-interactive-plot + seg_old = courbe_en_vlines.get_segments() + if ymin is None: + ymin = seg_old[0][0, 1] + if ymax is None: + ymax = seg_old[0][1, 1] + + seg_new = [np.array([[xx, ymin], + [xx, ymax]]) for xx in x] + + courbe_en_vlines.set_segments(seg_new) + +def retracer_parametree(echantillon, a, b): + y_max = max(echantillon) + if en_periode_de_retour: + T_emp = T_emp_parametre(len(echantillon), a, b) + update_vlines(courbe_estim_T_vlines, T_emp, ymin=0, ymax=y_max) + ax_T.set_title(f"T empirique pour a={a:.2f} et b={b:.2f} ") + else: + #freq = [ 1 - (1 / T) for T in T_emp] + freq = freq_emp_parametre(len(echantillon), a , b) + update_vlines(courbe_estim_T_vlines, freq, ymin=0, ymax=y_max) + ax_T.set_title(f"fréquence empirique pour a={a:.2f} et b={b:.2f} ") + + #ax.set_xbounds(lower=min(T_emp), upper=max(T_emp)) + # on retrace + fig_pp.canvas.draw_idle() + +def retracer_Gumbel(serie_Gumbel): + + if en_periode_de_retour: + courbe_Gumbel.set_xdata(SerieGumbel.T_Gumbel_plot) + else: + #freq = [ 1 - (1 / T) for T in T_emp] + freq = [ 1 - (1/T) for T in SerieGumbel.T_Gumbel_plot] + courbe_Gumbel.set_xdata(freq) + + # on retrace + fig_pp.canvas.draw_idle() + +def retracer_estim_Gumbel(serie_Gumbel): + gr, x0 = AjustementParametresDeGumbel(np.mean(serie_Gumbel), np.std(serie_Gumbel)) + if en_periode_de_retour: + T_Gumbel = [TdeQ_Gumbel(Q, gr, x0) for Q in serie_Gumbel] + courbe_estim_Gumbel.set_data(T_Gumbel, serie_Gumbel) + else: + # freq = [ 1 - (1 / T) for T in T_emp] + freq = freq_emp_parametre(len(serie_Gumbel), a, b) + courbe_estim_Gumbel.set_data(freq, serie_Gumbel) + + # on retrace + fig_pp.canvas.draw_idle() + + +# https://matplotlib.org/stable/gallery/widgets/slider_demo.html +# https://stackoverflow.com/questions/13656387/can-i-make-matplotlib-sliders-more-discrete +def update_a(val): + # val = valeur renvoyée par le widget + global a + a = val # comme on a précisé "global a", changer a dans la fonction = changer a du prog principal + retracer_parametree(serie_Gumbel.serie_triee, a, b) + +def update_b(val): + # val = valeur renvoyée par le widget + global b + b = val # comme on a précisé "global b", changer b dans la fonction = changer b du prog principal + retracer_parametree(serie_Gumbel.serie_triee, a, b) + +def update_series(event): + # bouton "ajouter des obs" activé + + # comme on a précisé "global N_total, echantillon", sa modifications sera propagée jusque dans le prog principal + global N_total + N_total += N_serie + serie_Gumbel.ajout_obs(N_serie) + + ax_chrono.clear() + ax_chrono.set_title(f"Qmax annuels observés sur {N_total} années") + courbe_Gumbel1, = ax_chrono.plot(range(len(serie_Gumbel.serie_chrono)), serie_Gumbel.serie_chrono, ls='--') + courbe_Gumbel = ax_chrono.scatter(range(len(serie_Gumbel.serie_chrono)), serie_Gumbel.serie_chrono, + c=serie_Gumbel.serie_couleurs, alpha=0.7, marker="*", s=20) + retracer_estim_Gumbel(serie_Gumbel.serie_triee) + fig_pp.canvas.draw_idle() + retracer_parametree(serie_Gumbel.serie_triee, a, b) + +def switch_freq(label): + global en_periode_de_retour + + en_periode_de_retour = not en_periode_de_retour + retracer_parametree(serie_Gumbel.serie_triee, a, b) + retracer_Gumbel(serie_Gumbel.serie_triee) + retracer_estim_Gumbel(serie_Gumbel.serie_triee) + if en_periode_de_retour: + ax_T.set_xlim(left=0, right=N_total * 1.1) + ax_T.set_xlabel("Période de retour, années") + else: + # freq = [ 1 - (1 / T) for T in T_emp] + ax_T.set_xlim(left=0, right=1.1) + ax_T.set_xlabel("fréquence de non-dépassement") + + fig_pp.canvas.draw_idle() + +#plt.ion() + +a_Tchego, b_Tchego = 0.3 , 0.4 # affectation de plusieurs valeurs simultanées par tuples +# initialisation des paramètres de départ (modifiables par curseur) +a = a_Tchego +b = b_Tchego + +# on va constituer une série d' "observations" par ajouts de N_series obs à la fois +N_serie = 10 +serie_Gumbel = SerieGumbel(x0=196, gr=102) +serie_Gumbel.ajout_obs(N_serie) + + +rangs = echantillon_uniforme(N_serie) + +N_total = N_serie +en_periode_de_retour = True + +print(serie_Gumbel.serie_chrono) + + +fig_pp = plt.figure() +gs = gridspec.GridSpec(16, 2) +ax_chrono = plt.subplot(gs[0:6, :]) +ax_T = plt.subplot(gs[6:12, :]) +ax_slide_a = plt.subplot(gs[15, 0]) +ax_slide_b = plt.subplot(gs[15, 1]) +ax_chb = plt.subplot(gs[14, 0]) +ax_bouton = plt.subplot(gs[14, 1]) + +# ax_chb = fig.add_subplot() +plt.subplots_adjust(wspace=1, hspace=0.5,left=0.1,top=0.85,right=0.9,bottom=0.1) +fig_pp.canvas.set_window_title("ScE - Hydrologie -Démo Gumbel") + +fig_pp.suptitle(f"Démo Gumbel : tirages et leurs périodes de retour 'empiriques") + +ax_T.set_xlabel("Période de retour (années)") + +# LES OBS +courbe_Gumbel1, = ax_chrono.plot(range(len(serie_Gumbel.serie_chrono)), serie_Gumbel.serie_chrono, ls='--') +courbe_Gumbel = ax_chrono.scatter(range(len(serie_Gumbel.serie_chrono)), serie_Gumbel.serie_chrono, c=serie_Gumbel.serie_couleurs, alpha=0.7, label= f"Q(t) observé sur {N_total} années, ", marker="*", s=20) + +#LA COURBE DE GUMBEL, inchangée (sauf si tracé en fréquence...) +courbe_Gumbel, = ax_T.plot(SerieGumbel.T_Gumbel_plot, serie_Gumbel.QGumbel_plot, color='red', alpha=0.7, linewidth=5, solid_capstyle='round', + zorder=10, label = "Q(T) du bassin versant", marker="None", ls='--', markersize=7) + +gr_est, x0_est = serie_Gumbel.gr_echantillon, serie_Gumbel.x0_echantillon + +courbe_estim_T_vlines = ax_T.vlines(x=T_emp_parametre(N_total, a, b), ymin=0, ymax=serie_Gumbel.serie_triee, color="purple", alpha=0.7, linewidth=2, + zorder=2, label= f"T ou f empirique, fonction de a et b", ls="--") +courbe_estim_Gumbel, = ax_T.plot([TdeQ_Gumbel(Q, gr_est, x0_est) for Q in serie_Gumbel.serie_triee], serie_Gumbel.serie_triee, color='blue', alpha=0.7, label=" ajustement Gumbel sur obs", + zorder=10, marker="x", markersize=7) + + +for ax_s in [ax_slide_a, ax_slide_b, ax_bouton, ax_chb]: + ax_s.xaxis.set_visible(False) + ax_s.yaxis.set_visible(False) + +for pos in ['right', 'top', 'bottom', 'left']: + ax_chb.spines[pos].set_visible(False) + +print("ici") + +# nom connu même hors de la fonction pour éviter le GC ? +# a +slider_a = Slider( + ax_slide_a, "paramètre a ", valmin=0.01, valmax = 0.99, valfmt='%0.2f', valinit=a, color="green") + +slider_a.on_changed(update_a) + +# b +slider_b = Slider( + ax_slide_b, "paramètre b ", valmin=0.01, valmax = 0.99, valfmt='%0.2f', valinit=b, color="blue") + +slider_b.on_changed(update_b) + +# bouton pour ajouter N_serie valeurs +valeurs_possibles_N = np.linspace(0, 100, 1) +bouton_obs = Button( + ax_bouton, f" Ajouter {N_serie} obs ", color="purple") + +bouton_obs.on_clicked(update_series) + +# switch T / freq + +chb_enT = CheckButtons(ax_chb, [ "axe des x en fréquence (de 0 à 1)"], [False]) +""" +for rect in chb_enT.rectangles: + rect.set_width(0.1) + rect.set_height(1) + rect.xy = (0.05,0) +""" + +chb_enT.on_clicked(switch_freq) + + +ax_T.legend(title="formules", bbox_to_anchor=(1.05, 0.10), loc='lower right') +#plt.tight_layout() +#fig_pp.savefig("ma_figure.png") +#fig_pp.canvas.draw() +plt.show() +#plt.ioff() -- GitLab