#!/usr/bin/python3
# -*- coding: utf-8 -*-
########################################################################
#
# This file is part of python module <pyspc>.
# Copyright (C) 2013-2021 R. Marty
# (renaud.marty@developpement-durable.gouv.fr)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program (see COPYING.txt).
# If not, see <http://www.gnu.org/licenses/>.
#
########################################################################
"""
Modélisations hydrologiques - GRP version 2022 - Rapports PDF
"""
from datetime import datetime as dt
import os.path
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
from matplotlib.backends.backend_pdf import PdfPages
import numpy as np
import pandas as pnd
from pyspc import __date__, __version__
import pyspc.core.exception as _exception
from pyspc.convention.grp22 import (
CAL_BASIN_OPTIONS, CAL_VERIF_NAMES, CAL_VERIF_SCORES)
from pyspc.model.grp22.cal_basin import GRP_Basin
from pyspc.model.grp22.cal_config import GRP_Run
from pyspc.model.grp22.cal_verif import GRP_Verif
from pyspc.core.config import Config
RADAR_COLORS = ['tab:olive', 'tab:orange', 'tab:red']
[docs]
def report_model(filename=None, run=None, basin=None, list_rr=None,
list_ta=None, hypso=None, hypso_lim=None):
"""
Rapport PDF décrivant les paramètres de la modélisation GRP v2022
Parameters
----------
filename : str
Chemin du fichier PDF à imprimer
run : pyspc.model.grp22.GRP_Run
Information générale du calage du modèle
basin : pyspc.model.grp22.GRP_Basin
Information spécifique du modèle
list_rr : str
Chemin du fichier LISTE_PLUVIOMETRES.ini
list_ta : str
Chemin du fichier LISTE_TEMPERATURES.ini
hypso : pandas.DataFrame
Courbe hypsométrique (colonnes: ['pct', 'Z'])
hypso_lim : tuple
Limites des altitudes
Returns
-------
filename : str
Chemin du fichier PDF à imprimer
fig : matplotlib.pyplot.figure
Si le chemin n'est pas spécifié
Notes
-----
Si le paramètre basin est complété par les périodes de données,
Alors le rapport en tient compte. Ces compléments doivent respecter:
- basin['MODELISATION']['start'] : datetime.datetime du départ des données
- basin['MODELISATION']['end'] : datetime.datetime de la fin des données
- basin['MODELISATION']['mv'] : float du ratio de valeurs manquantes
- basin[station]['rr_start'] : datetime.datetime du départ 'P'
- basin[station]['rr_end'] : datetime.datetime de la fin 'P'
- basin[station]['rr_mv'] : float du ratio de valeurs manquantes 'P'
- basin[station]['ta_start'] : datetime.datetime du départ 'T'
- basin[station]['ta_end'] : datetime.datetime de la fin 'T'
- basin[station]['ta_mv'] : float du ratio de valeurs manquantes 'T'
See Also
--------
pyspc.model.grp22.GRP_Basin
pyspc.model.grp22.GRP_Cfg
pyspc.model.grp22.GRP_Data
pyspc.model.grp22.GRP_Run
"""
# ---------------------------------------------------------------------
# 0 - Contrôles
# ---------------------------------------------------------------------
_exception.raise_valueerror(not isinstance(run, GRP_Run),
"Paramètre 'run' incorrect")
_exception.raise_valueerror(not isinstance(basin, GRP_Basin),
"Paramètre 'basin' incorrect")
basin = _add_list_rrta('rr', list_rr, basin)
basin = _add_list_rrta('ta', list_ta, basin)
list_P = [s for s in basin if CAL_BASIN_OPTIONS['P']['w'] in basin[s]]
list_T = [s for s in basin if CAL_BASIN_OPTIONS['T']['w'] in basin[s]]
# ---------------------------------------------------------------------
# 1 - Création de la figure et de la zone graphique
# ---------------------------------------------------------------------
fig = plt.figure(constrained_layout=False, figsize=(8.27, 11.69), dpi=150)
gs = GridSpec(nrows=4, ncols=2, figure=fig,
left=0.08, right=0.95, top=0.92, bottom=0.03,
hspace=0.25, wspace=0.20)
# ---------------------------------------------------------------------
# 2 - Information run
# ---------------------------------------------------------------------
axr = fig.add_subplot(gs[:2, :-1])
df = pnd.DataFrame(run)
df.index = list(run._fields)
df.columns = ['MODELE']
df = df.T
df.DEB = df.DEB.apply(
lambda x: dt.strptime(x, '%d/%m/%Y %H:%M').strftime('%Y-%m-%d'))
df.FIN = df.FIN.apply(
lambda x: dt.strptime(x, '%d/%m/%Y %H:%M').strftime('%Y-%m-%d'))
df['Ratio MV'] = f"{basin['MODELISATION']['mv']:.2f}"
df = df.T
df2table(df, axr, pad=0.12)
# ---------------------------------------------------------------------
# 3 - Information hypso
# ---------------------------------------------------------------------
if hypso is not None:
axh = fig.add_subplot(gs[:2, -1])
axh.plot(hypso.pct, hypso.Z,
linewidth=2, marker='o', color='tab:blue')
axh.set_xlim(0, 100)
axh.set_xlabel('Pourcentage Bassin')
if hypso_lim is not None:
axh.set_ylim(hypso_lim)
axh.set_ylabel('Altitude [m]')
# ---------------------------------------------------------------------
# 4 - Information P
# ---------------------------------------------------------------------
if list_P:
axp = fig.add_subplot(gs[-2, :])
df = pnd.DataFrame({s: {o: basin[s][o]
for o in basin[s] if o.startswith('rr')}
for s in list_P}).T
df.index.name = 'code'
df = df.reset_index()
df = df[['code', 'rr_NOM', 'rr_pdt', 'rr_pond',
'rr_start', 'rr_end', 'rr_mv']]
# # df.start = df.start.apply(lambda x: x.strftime('%Y-%m-%d'))
# # df.end = df.end.apply(lambda x: x.strftime('%Y-%m-%d'))
df.rr_start = pnd.to_datetime(
df.rr_start, errors='coerce').dt.strftime('%Y-%m-%d')
df.rr_end = pnd.to_datetime(
df.rr_end, errors='coerce').dt.strftime('%Y-%m-%d')
# df.mv = df.mv.apply(lambda x: '{0:.2f}'.format(x))
df.rr_mv = df.rr_mv.apply('{0:.2f}'.format) # pylint: disable=consider-using-f-string
df.columns = ['Code', 'Nom', 'Pas de Temps', 'Pct',
'Début', 'Fin', 'Pct MV']
df2table(df, axp)
# ---------------------------------------------------------------------
# 5 - Information T
# ---------------------------------------------------------------------
if list_T:
axt = fig.add_subplot(gs[-1, :])
df = pnd.DataFrame({s: {o: basin[s][o]
for o in basin[s] if o.startswith('ta')}
for s in list_T}).T
df.index.name = 'code'
df = df.reset_index()
df = df[['code', 'ta_NOM', 'ta_alti', 'ta_pond',
'ta_start', 'ta_end', 'ta_mv']]
# df.start = df.start.apply(lambda x: x.strftime('%Y-%m-%d'))
# df.end = df.end.apply(lambda x: x.strftime('%Y-%m-%d'))
df.ta_start = pnd.to_datetime(
df.ta_start, errors='coerce').dt.strftime('%Y-%m-%d')
df.ta_end = pnd.to_datetime(
df.ta_end, errors='coerce').dt.strftime('%Y-%m-%d')
# df.mv = df.mv.apply(lambda x: '{0:.2f}'.format(x))
df.ta_mv = df.ta_mv.apply('{0:.2f}'.format) # pylint: disable=consider-using-f-string
df.columns = ['Code', 'Nom', 'Altitude [m]', 'Pct',
'Début', 'Fin', 'Pct MV']
df2table(df, axt)
# ---------------------------------------------------------------------
# 6 - Création du fichier PDF
# https://matplotlib.org/3.1.0/users/prev_whats_new/whats_new_2.1.0.html#metadata-savefig-keyword-argument
# ---------------------------------------------------------------------
plt.suptitle(f"Modélisation GRP v2022 - Bassin {run.CODE} - "
f"Pas de temps {run.PDT}", fontsize=14, fontweight='bold')
if filename is not None:
plt.savefig(
filename, dpi=150,
metadata={'Title': f'{run.NOM} ({run.CODE}, {run.PDT})',
'Author': f'pyspc-{__version__} ({__date__})'})
plt.close(fig)
return filename
return fig
def _add_list_rrta(varname, filename, basin):
"""
Ajouter les informations de LISTE_PLUVIOMTERES et LISTE_TEMPERATURES.
"""
if isinstance(filename, str) and os.path.exists(filename):
config = Config(filename)
config.read()
else:
config = None
for s in basin:
if isinstance(config, dict) and s in config:
for o in config[s]:
basin[s][f'{varname}_{o}'] = config[s][o]
return basin
[docs]
def report_verif(run=None, src_dirname=None, datatype=None, dest_dirname=None):
"""
Rapport CSV synthétisant les performances du modèle
Rapport PDF synthétisant les performances du modèle
Parameters
----------
run : pyspc.model.grp22.GRP_Run
Information générale du calage du modèle
src_dirname : str
Répertoire où chercher les fiches de performance
datatype : str
Nom du type de fiche de performance
dest_dirname : str
Répertoire de destination des rapports
Returns
-------
filenames : list
Fichiers écrits
Notes
-----
Si datatype = 'cal', le document pdf contient 2 pages :
le tableau et les histogrammes.
Si datatype = 'rtime', le document pdf contient 1 page :
le tableau avec la vision radar.
See Also
--------
pyspc.model.grp22.cal_verif.GRP_Verif
pyspc.model.grp22.cal_verif.GRP_Verif.concat
"""
# ---------------------------------------------------------------------
# 0- Contrôles
# ---------------------------------------------------------------------
_exception.raise_valueerror(not isinstance(run, GRP_Run),
"Paramètre 'run' incorrect")
_exception.check_str(src_dirname)
_exception.check_str(dest_dirname)
GRP_Verif.check_datatype(datatype=datatype)
filenames = []
# ---------------------------------------------------------------------
# 1 - Synthèse CSV des fiches de performance
# ---------------------------------------------------------------------
df = GRP_Verif.concat(
loc=run.CODE, timestep=run.PDT, dirname=src_dirname, datatype=datatype)
f = os.path.join(
dest_dirname, f'synthese_{run.CODE}_{run.PDT}_{datatype}.csv')
df.to_csv(f, sep=';', float_format='%.3f')
filenames.append(f)
# ---------------------------------------------------------------------
# 2 - Synthèse PDF des fiches de performance
# ---------------------------------------------------------------------
dfh = df.set_index(['SA_RT', 'HOR', 'SC'])
dfh.POD = dfh.POD / 100.
dfh.FAR = dfh.FAR / 100.
dfh.CSI = dfh.CSI / 100.
svs = sorted(dfh.SV.unique())
f = os.path.join(
dest_dirname, f'synthese_{run.CODE}_{run.PDT}_{datatype}.pdf')
title = f'{run.NOM} ({run.CODE}, {run.PDT})\n{CAL_VERIF_NAMES[datatype]}'
with PdfPages(f) as pdf:
# -----------------------------------------------------------------
# 2.1 - page 1 - Tableau des valeurs
# Radar plot si Calage complet
# -----------------------------------------------------------------
if datatype == 'rtime':
dfr = dfh.reset_index(drop=True).set_index(['SV']).fillna(0)
fig = plt.figure(
constrained_layout=False, figsize=(8.27, 11.69), dpi=150)
gs = GridSpec(nrows=6, ncols=1, figure=fig,
left=0.15, right=0.85, top=0.90, bottom=0.20,
hspace=0.01, wspace=0.01)
ax = fig.add_subplot(gs[0, 0])
axr = fig.add_subplot(gs[1:, 0], projection="polar")
theta = np.arange(
len(dfr.columns) + 1) / float(len(dfr.columns)) * 2 * np.pi
kc = 0
axr.set_theta_direction('clockwise') # Sens horaire
axr.set_theta_offset(1.70 * np.pi) # Décaler
axr.set_rlabel_position(180) # Angle des labels 'rayon'
axr.set_rgrids(
[x / 10 for x in range(1, 10)],
[f'{x / 10:.1f}' for x in range(1, 10)],
color=[0.20, 0.20, 0.20], fontsize=8
)
for row in dfr.iterrows():
label = f'SV = {row[0]} $m^3/s$'
color = RADAR_COLORS[kc]
kc += 1
values = row[1].values
values = np.append(values, values[0])
# draw the polygon and the mark the points
# for each angle/value combination
_, = axr.plot(
theta, values, color=color, marker="o", label=label)
# Summits labels
plt.xticks(theta[:-1], dfr.columns, color='black', size=12)
# axr.set_ticks(theta[:-1])
# axr.set_ticklabels(toprint_varlabels)
# to increase the distance of the labels to the plot
axr.tick_params(pad=15)
# fill the area of the polygon with green and some transparency
axr.fill(theta, values, color, alpha=0.3)
axr.set_ylim(0, 1)
axr.legend(bbox_to_anchor=(1.05, 1.05))
else:
fig, ax = plt.subplots(figsize=(8.27, 11.69))
df2table(df, ax, fontsize=12, pad=0.15)
plt.suptitle(title, fontsize=14, fontweight='bold')
pdf.savefig(fig, bbox_inches='tight')
plt.close(fig)
# -----------------------------------------------------------------
# 2.2 - Page 2 - Histogrammes uniquement si calage-validation
# -----------------------------------------------------------------
if datatype == 'cal':
axsx = None
axsy = None
fig = plt.figure(
constrained_layout=False, figsize=(11.69, 8.27), dpi=150)
gs = GridSpec(nrows=len(svs), ncols=len(CAL_VERIF_SCORES),
figure=fig,
left=0.08, right=0.95, top=0.85, bottom=0.03,
hspace=0.25, wspace=0.10)
for kv, sv in enumerate(svs):
for ks, ss in enumerate(CAL_VERIF_SCORES):
ax = fig.add_subplot(gs[kv, ks], sharex=axsx, sharey=axsy)
if axsx is None:
axsx = ax
if axsy is None:
axsy = ax
subdf = dfh[dfh.SV == sv]
subdf.plot.bar(ax=ax, y=ss, rot=90, legend=False,
color=index2colors(subdf.index))
if ks == int(len(CAL_VERIF_SCORES) / 2):
axtitle = r'$\bf Seuil$ = ' + \
f'{sv:.2f}' + r' $\bf m^3/s$' + '\n' + ss
else:
axtitle = '\n' + ss
ax.set_title(axtitle)
ax.tick_params(axis='x', which='major', labelsize=8)
ax.set_ylim(0, 1)
plt.suptitle(title, fontsize=14, fontweight='bold')
pdf.savefig(fig, bbox_inches='tight')
plt.close(fig)
# -----------------------------------------------------------------
# Ajout de méta-données
# -----------------------------------------------------------------
# We can also set the file's metadata via the PdfPages object:
# https://matplotlib.org/stable/gallery/misc/multipage_pdf.html
d = pdf.infodict()
# d['Title'] = 'Verification Report\n' + title
d['Title'] = f'{run.NOM} ({run.CODE}, {run.PDT})'
d['Author'] = f'pyspc-{__version__} ({__date__})'
filenames.append(f)
return filenames
def index2colors(index):
"""
Définir la couleur de l'histogramme vertical selon SMN/AMN et TAN/RNA
"""
colors_dict = {'SMN_TAN': 'tab:blue', 'SMN_RNA': 'tab:red',
'AMN_TAN': 'tab:cyan', 'AMN_RNA': 'tab:orange'}
colors = []
for i in index:
colors.append(colors_dict.get(i[0], 'tab:brown'))
return colors
def df2table(df, ax, loc='upper center', fontsize=10, pad=None):
"""
Imprimer un pandas.DataFrame comme tableau dans une image
Parameters
----------
df : pandas.DataFrame
Tableau de données
ax : matplotlib.axes.Axes
Zone graphique où insérer le tableau
loc : str
alignement du tableau
fontsize : int
Taille de la police
pad : float
Espace entre le texte et le bord de la cellule
Par défaut: matplotlib.table.Cell.PAD
Returns
-------
table : matplotlib.table.Table
Tableau imprimé
Notes
-----
https://stackoverflow.com/a/72957628
https://stackoverflow.com/a/60307690
https://stackoverflow.com/a/64595057
"""
alternating_colors = [['white'] * len(df.columns),
['lightgray'] * len(df.columns)] * len(df)
alternating_colors = alternating_colors[:len(df)]
ax.axis('tight')
ax.axis('off')
table = ax.table(cellText=df.values,
rowLabels=df.index,
colLabels=df.columns,
rowColours=['lightblue']*len(df),
colColours=['lightblue']*len(df.columns),
cellColours=alternating_colors,
loc=loc)
table.auto_set_font_size(False)
table.set_fontsize(fontsize)
# Provide integer list of columns to adjust
table.auto_set_column_width(col=list(range(len(df.columns))))
if pad is not None:
for c in table.get_celld().values():
rp = pad / c.PAD
h = rp * c.get_height()
c.set_height(h)
return table