Exemple de cas d’utilisation

Cet exemple illustre un cas d’utilisation qui couvre les étapes essentielles de la construction d’un modèle hydrologique et de l’analyse de l’impact des changements climatiques :

  • Identification du bassin versant et de ses principales caractéristiques

    • Bassin versant de la rivière Beaurivage dans le sud du Québec, à l’emplacement de la station 023401.

  • Collecte des données observées

    • Données ERA5-Land et données de débit.

  • Préparation et calage du modèle hydrologique

    • GR4JCN émulé par le cadre hydrologique Raven.

  • Calcul des indicateurs hydrologiques

    • Débit moyen estival

    • Débit moyen mensuel

    • Débit maximum sur 20 et 100 ans

    • Débit estival minimal sur 7 jours de récurrence 2 ans

  • Évaluation de l’impact des changements climatiques

    • Simulations CMIP6 post-traitées du jeu de données ESPO-G6-R2

Identification d’un bassin versant et de ses caractéristiques

INFO

Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook GIS.

Cette première étape dépend fortement du modèle hydrologique. Puisque nous utiliserons GR4JCN dans notre exemple, nous devons obtenir l’aire de drainage, les coordonnées du centroïde et l’altitude. Nous aurons également besoin de la délimitation du bassin versant pour extraire les données météorologiques. Ces informations proviendront de HydroBASINS via le module xhydro.gis.

  • Délimitation du bassin versant et caractéristiques de base : xhydro.gis.watershed_delineation et xhydro.gis.watershed_properties

  • Altitude : xhydro.gis.surface_properties

[1]:
# Workaround for determining the notebook folder within a running notebook
# This cell is not visible when the documentation is built.

from __future__ import annotations

try:
    from _finder import _find_current_folder

    notebook_folder = _find_current_folder()
except ImportError:
    from pathlib import Path

    notebook_folder = Path().cwd()

(notebook_folder / "_data").mkdir(exist_ok=True)
[2]:
import leafmap
import numpy as np
import pandas as pd

import xhydro.gis as xhgis
[3]:
# Watershed delineation
coords = (-71.28878, 46.65692)
m = leafmap.Map(center=(coords[1], coords[0]), zoom=8)
gdf = xhgis.watershed_delineation(coordinates=coords, map=m)
gdf
[3]:
HYBAS_ID Upstream Area (sq. km). geometry category color
0 7120365812 693.5 POLYGON ((-71.09758 46.40035, -71.09409 46.403... 1 #ffffd9

Afficher m produira cette carte : image.png

[4]:
# Watershed properties
gdf_wat = xhgis.watershed_properties(gdf)
gdf_wat
[4]:
HYBAS_ID Upstream Area (sq. km). category color area perimeter gravelius centroid
0 7120365812 693.5 1 #ffffd9 5.845762e+08 158155.350574 1.845263 (-71.26046365140014, 46.45216102567744)
[5]:
# Surface properties
gdf_surf = xhgis.surface_properties(gdf)
gdf_surf
[5]:
elevation slope aspect time id proj:transform platform epsg proj:shape proj:epsg title band gsd spatial_ref
geometry
0 224.201508 1.416482 214.740387 2021-04-22 Copernicus_DSM_COG_30_N46_00_W072_00_DEM {0.0, -0.0008333333333333334, 47.0004166666666... TanDEM-X 4326 {1200} 4326 N46_00_W072_00 data 90 0

Nous conserverons les informations nécessaires et les formaterons pour GR4JCN.

[6]:
drainage_area = np.array([gdf_wat["Upstream Area (sq. km)."].iloc[0]])
latitude = np.array([gdf_wat.centroid[0][1]])
longitude = np.array([gdf_wat.centroid[0][0]])
elevation = np.array([gdf_surf["elevation"].iloc[0]])

Puisque xhgis.watershed_delineation extrait le polygone HydroBASINS le plus proche, le bassin versant pourrait ne pas correspondre exactement aux coordonnées demandées. La station de jaugeage 023401 a une aire de drainage associée de 708 km², ce qui diffère de nos résultats. Le débit devra être ajusté en utilisant un facteur d’échelle.

[7]:
gauge_area = 708
scaling_factor = drainage_area / gauge_area
scaling_factor
[7]:
array([0.97951977])

Collecte des données observées

[8]:
import geopandas as gpd
import matplotlib.pyplot as plt
import xarray as xr

# For easy access to the specific streamflow data used here
import xdatasets
import xscen

Données météorologiques

INFO

Plusieurs librairies peuvent être utilisées pour effectuer ces étapes. Par souci de simplicité, cet exemple utilisera les modules subset et aggregate de la librairie xscen.

Cet exemple utilisera les données journalières ERA5-Land hébergées sur la plateforme PAVICS.

[9]:
# Extraction of ERA5-Land data
meteo_ref = xr.open_dataset(
    "https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/datasets/reanalyses/day_ERA5-Land_NAM.ncml",
    engine="netcdf4",
    chunks={"time": 365, "lon": 50, "lat": 50},
)[["pr", "tasmin", "tasmax"]]
meteo_ref
[9]:
<xarray.Dataset> Size: 429GB
Dimensions:  (time: 26298, lat: 800, lon: 1700)
Coordinates:
  * lat      (lat) float32 3kB 10.0 10.1 10.2 10.3 10.4 ... 89.6 89.7 89.8 89.9
  * lon      (lon) float32 7kB -179.9 -179.8 -179.7 -179.6 ... -10.2 -10.1 -10.0
  * time     (time) datetime64[ns] 210kB 1950-01-01 1950-01-02 ... 2021-12-31
Data variables:
    pr       (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray>
    tasmin   (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray>
    tasmax   (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray>
Attributes: (12/26)
    Conventions:          CF-1.8
    cell_methods:         time: mean (interval: 1 day)
    data_specs_version:   00.00.07
    domain:               NAM
    format:               netcdf
    frequency:            day
    ...                   ...
    dataset_description:  https://www.ecmwf.int/en/era5-land
    license_type:         permissive
    license:              Please acknowledge the use of ERA5-Land as stated i...
    attribution:          Contains modified Copernicus Climate Change Service...
    citation:             Muñoz Sabater, J., (2021): ERA5-Land hourly data fr...
    doi:                  https://doi.org/10.24381/cds.e2161bac

Ce jeu de données couvre l’ensemble du globe et possède plus de 70 ans de données. La première étape consistera donc à sous-échantillonner les données spatialement et temporellement. Pour le sous-échantillonnage spatial, le GeoDataFrame obtenu précédemment peut être utilisé.

[10]:
meteo_ref = meteo_ref.sel(time=slice("1991", "2020"))  # Temporal subsetting
meteo_ref = xscen.spatial.subset(
    meteo_ref, method="shape", tile_buffer=2, shape=gdf
)  # Spatial subsetting, with a buffer of 2 grid cells
meteo_ref = meteo_ref.assign_coords({"crs": meteo_ref.crs})
meteo_ref
[10]:
<xarray.Dataset> Size: 9MB
Dimensions:  (time: 10958, lat: 8, lon: 8)
Coordinates:
  * lat      (lat) float32 32B 46.1 46.2 46.3 46.4 46.5 46.6 46.7 46.8
  * lon      (lon) float32 32B -71.6 -71.5 -71.4 -71.3 -71.2 -71.1 -71.0 -70.9
  * time     (time) datetime64[ns] 88kB 1991-01-01 1991-01-02 ... 2020-12-31
    crs      int64 8B 1
Data variables:
    pr       (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray>
    tasmin   (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray>
    tasmax   (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray>
Attributes: (12/27)
    Conventions:          CF-1.8
    cell_methods:         time: mean (interval: 1 day)
    data_specs_version:   00.00.07
    domain:               NAM
    format:               netcdf
    frequency:            day
    ...                   ...
    license_type:         permissive
    license:              Please acknowledge the use of ERA5-Land as stated i...
    attribution:          Contains modified Copernicus Climate Change Service...
    citation:             Muñoz Sabater, J., (2021): ERA5-Land hourly data fr...
    doi:                  https://doi.org/10.24381/cds.e2161bac
    crs:                  EPSG:4326
[11]:
ax = plt.subplot(1, 1, 1)
meteo_ref.tasmin.isel(time=0).plot(ax=ax)
gdf.plot(ax=ax)
[11]:
<Axes: title={'center': 'time = 1991-01-01, crs = 1'}, xlabel='longitude [degrees_east]', ylabel='latitude [degrees_north]'>
../_images/notebooks_use_case_17_1.png

Puisque GR4JCN est un modèle global, nous devons moyenner les données météorologiques sur le bassin versant. Cela sera accompli en utilisant xscen.spatial_mean. Il existe plusieurs méthodes, mais pour les fichiers shapefile, la plus précise est « xesmf », qui utilise le SpatialAverager de xESMF pour suivre précisément la représentation de chaque point de grille sur le polygone. Cela peut être très lent pour des polygones détaillés avec de nombreux sommets, mais l’argument « simplify_tolerance » aide à accélérer ce processus.

[12]:
meteo_ref = xscen.spatial_mean(
    meteo_ref,
    method="xesmf",
    region={"method": "shape", "shape": gdf},
    simplify_tolerance=0.1,
    kwargs={"skipna": True},
)

# Some housekeeping to associate the HYBAS_ID dimension (coming from the shapefile) to other coordinates added from the GeoDataFrame.
for c in meteo_ref.coords:
    if len(meteo_ref[c].dims) == 0:
        if c not in ["lon", "lat"]:
            meteo_ref = meteo_ref.drop_vars([c])
        else:
            meteo_ref[c] = meteo_ref[c].expand_dims("HYBAS_ID")
meteo_ref
[12]:
<xarray.Dataset> Size: 351kB
Dimensions:   (time: 10958, HYBAS_ID: 1)
Coordinates:
  * time      (time) datetime64[ns] 88kB 1991-01-01 1991-01-02 ... 2020-12-31
    lon       (HYBAS_ID) float64 8B -71.28
    lat       (HYBAS_ID) float64 8B 46.46
  * HYBAS_ID  (HYBAS_ID) int64 8B 7120365812
Data variables:
    pr        (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray>
    tasmin    (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray>
    tasmax    (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray>
Attributes: (12/28)
    Conventions:          CF-1.8
    cell_methods:         time: mean (interval: 1 day)
    data_specs_version:   00.00.07
    domain:               NAM
    format:               netcdf
    frequency:            day
    ...                   ...
    license:              Please acknowledge the use of ERA5-Land as stated i...
    attribution:          Contains modified Copernicus Climate Change Service...
    citation:             Muñoz Sabater, J., (2021): ERA5-Land hourly data fr...
    doi:                  https://doi.org/10.24381/cds.e2161bac
    crs:                  EPSG:4326
    regrid_method:        conservative

Raven attend des températures en Celsius et des précipitations en millimètres, mais elles sont actuellement conforme aux conventions CF, en Kelvin et en kg m-2 s-1, respectivement. Encore une fois, xscen peut être utilisé pour cette opération.

[13]:
meteo_ref = xscen.utils.change_units(
    meteo_ref, {"tasmax": "degC", "tasmin": "degC", "pr": "mm"}
)
[14]:
# Save
meteo_ref.to_netcdf(notebook_folder / "_data" / "meteo.nc")

Données hydrométriques

Les données de débit du gouvernement du Québec sont accessibles via la librairie xdatasets.

[15]:
qobs = (
    xdatasets.Query(
        **{
            "datasets": {
                "deh": {
                    "id": ["023401"],
                    "variables": ["streamflow"],
                }
            },
            "time": {"start": "1991-01-01", "end": "2020-12-31"},
        }
    )
    .data.squeeze()
    .load()
).sel(spatial_agg="watershed")
[16]:
# Once again, some housekeeping is required on the metadata to make sure that xHydro understands the dataset.
qobs["id"].attrs["cf_role"] = "timeseries_id"
qobs = qobs.rename({"id": "station_id"})
qobs["streamflow"].attrs = {
    "long_name": "Streamflow",
    "units": "m3 s-1",
    "standard_name": "water_volume_transport_in_river_channel",
    "cell_methods": "time: mean",
}
for c in qobs.coords:
    if len(qobs[c].dims) == 0 and "time" not in c:
        qobs[c] = qobs[c].expand_dims("station_id")

qobs
[16]:
<xarray.Dataset> Size: 132kB
Dimensions:        (time: 10958, station_id: 1)
Coordinates: (12/15)
    drainage_area  (station_id) float32 4B 708.0
    end_date       (station_id) datetime64[ns] 8B 2020-12-31
  * station_id     (station_id) <U6 24B '023401'
    latitude       (station_id) float32 4B 46.66
    longitude      (station_id) float32 4B -71.29
    name           (station_id) object 8B 'Beaurivage'
    ...             ...
    spatial_agg    (station_id) <U9 36B 'watershed'
    start_date     (station_id) datetime64[ns] 8B 1991-01-01
  * time           (time) datetime64[ns] 88kB 1991-01-01 ... 2020-12-31
    time_agg       <U4 16B 'mean'
    timestep       <U1 4B 'D'
    variable       (station_id) <U10 40B 'streamflow'
Data variables:
    streamflow     (time) float32 44kB 46.0 33.9 26.2 21.0 ... 19.98 15.76 12.93
[17]:
plt.figure(figsize=[10, 5])
qobs.streamflow.plot()
[17]:
[<matplotlib.lines.Line2D at 0x7fa33872faa0>]
../_images/notebooks_use_case_26_1.png

Comme spécifié plus tôt, les observations de débit doivent être modifiées pour tenir compte des différences de taille des bassins versants entre la jauge et le polygone.

[18]:
with xr.set_options(keep_attrs=True):
    qobs["streamflow"] = qobs["streamflow"] * scaling_factor
[19]:
# Save (necessary for model calibration)
qobs.to_netcdf(notebook_folder / "_data" / "qobs.nc")

Préparation et calage du modèle hydrologique (xhydro.modelling)

INFO

Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook de modélisation hydrologique.

La fonction perform_calibration nécessite un argument model_config qui lui permet de construire le modèle hydrologique correspondant. Toutes les informations requises ont été acquises dans les sections précédentes, il ne reste plus qu’à remplir les entrées du modèle RavenPy/GR4JCN.

Pour simplifier, l’altitude de la station météorologique sera considérée comme l’altitude moyenne du bassin versant réel. Le calcul de l’altitude des points de grille dans ERA5-Land n’est pas toujours trivial et ne fait pas partie de l’exemple. De même, l’équivalent en eau de neige n’est actuellement pas disponible dans la base de données de PAVICS, donc « AVG_ANNUAL_SNOW » a été estimé approximativement à l’aide de Brown & Brasnett (2010).

[21]:
# Model configuration
model_config = {
    "model_name": "GR4JCN",
    "parameters": [0.529, -3.396, 407.29, 1.072, 16.9, 0.947],
    "drainage_area": drainage_area,
    "elevation": elevation,
    "latitude": latitude,
    "longitude": longitude,
    "start_date": "1991-01-01",
    "end_date": "2020-12-31",
    "meteo_file": notebook_folder / "_data" / "meteo.nc",
    "data_type": ["TEMP_MAX", "TEMP_MIN", "PRECIP"],
    "alt_names_meteo": {"TEMP_MIN": "tasmin", "TEMP_MAX": "tasmax", "PRECIP": "pr"},
    "meteo_station_properties": {
        "ALL": {"elevation": elevation, "latitude": latitude, "longitude": longitude}
    },
    "rain_snow_fraction": "RAINSNOW_DINGMAN",
    "evaporation": "PET_HARGREAVES_1985",
    "global_parameter": {"AVG_ANNUAL_SNOW": 100.00},
}

# Parameter bounds for GR4JCN
bounds_low = [0.01, -15.0, 10.0, 0.0, 1.0, 0.0]
bounds_high = [2.5, 10.0, 700.0, 7.0, 30.0, 1.0]
[22]:
# Calibration / validation period
mask_calib = xr.where(qobs.time.dt.year <= 2010, 1, 0).values
mask_valid = xr.where(qobs.time.dt.year > 2010, 1, 0).values

# Model calibration
best_parameters, best_simulation, best_objfun = perform_calibration(
    model_config,
    "kge",
    qobs=notebook_folder / "_data" / "qobs.nc",
    bounds_low=bounds_low,
    bounds_high=bounds_high,
    evaluations=8,
    algorithm="DDS",
    mask=mask_calib,
    sampler_kwargs=dict(trials=1),
)
Initializing the  Dynamically Dimensioned Search (DDS) algorithm  with  8  repetitions
The objective function will be maximized
Starting the DDS algotrithm with 8 repetitions...
Finding best starting point for trial 1 using 5 random samples.
1 of 8, maximal objective function=0.164749, time remaining: 00:00:30
Initialize database...
['csv', 'hdf5', 'ram', 'sql', 'custom', 'noData']
2 of 8, maximal objective function=0.164749, time remaining: 00:00:30
3 of 8, maximal objective function=0.239819, time remaining: 00:00:25
4 of 8, maximal objective function=0.466832, time remaining: 00:00:20
5 of 8, maximal objective function=0.466832, time remaining: 00:00:14
6 of 8, maximal objective function=0.466832, time remaining: 00:00:07
7 of 8, maximal objective function=0.481349, time remaining: 00:00:00
8 of 8, maximal objective function=0.481349, time remaining: 23:59:53
Best solution found has obj function value of 0.48134887704825247 at 5



*** Final SPOTPY summary ***
Total Duration: 63.2 seconds
Total Repetitions: 8
Maximal objective value: 0.481349
Corresponding parameter setting:
param0: 0.46099
param1: -9.77586
param2: 216.456
param3: 0.384076
param4: 13.2655
param5: 0.563789
******************************

Best parameter set:
param0=0.4609896022995832, param1=-9.775860334349964, param2=216.455502549914, param3=0.38407572808259516, param4=13.265462690482325, param5=0.5637885058813862
Run number 6 has the highest objectivefunction with: 0.4813

Pour réduire les temps de calcul dans cet exemple, seulement 10 étapes ont été utilisées pour la fonction de calage, ce qui est bien en dessous du nombre recommandé. Les paramètres ci-dessous ont été obtenus en exécutant le code ci-dessus avec 150 évaluations.

[23]:
# Replace the results with parameters obtained using 150 evaluations
best_parameters = [
    0.3580270511815579,
    -2.187141388684563,
    24.012067980309702,
    0.000781,
    1.9330212374187332,
    0.5491789347783598,
]
model_config["parameters"] = best_parameters

best_simulation = xh.modelling.hydrological_model(model_config).run()

Le vrai KGE doit être calculé à partir d’une période de validation, en utilisant get_objective_function.

[24]:
get_objective_function(
    qobs=qobs.streamflow,
    qsim=best_simulation,
    obj_func="kge",
    mask=mask_valid,
).values
[24]:
array(0.69988518)
[25]:
ax = plt.figure(figsize=(10, 5))
qobs.streamflow.plot(color="k", linewidth=3)
best_simulation.streamflow.plot(color="r")
[25]:
[<matplotlib.lines.Line2D at 0x7fa314379790>]
../_images/notebooks_use_case_39_1.png

Calcul des indicateurs hydroclimatologiques

[26]:
import xclim

import xhydro.frequency_analysis as xhfa

Indicateurs non fréquenciels

INFO

Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse de l’impact des changements climatiques.

INFO

Les indicateurs personnalisés dans xHydro sont construits en suivant le formatage YAML requis par xclim. »

Un indicateur personnalisé construit avec xclim.core.indicator.Indicator.from_dict nécessitera ces éléments

  • « data » : Un dictionnaire avec les informations suivantes :

    • « base » : L“« ID YAML » obtenu depuis cette page.

    • « input » : Un dictionnaire reliant l’entrée xclim par défaut au nom de votre variable. Nécessaire uniquement si elle diffère. Dans le lien ci-dessus, ce sont les chaînes suivant « Uses: ».

    • « parameters » : Un dictionnaire contenant tous les autres paramètres pour un indicateur donné. Dans le lien ci-dessus, la manière la plus simple d’y accéder est de cliquer sur le lien dans le coin supérieur droit de la boîte décrivant un indicateur donné.

    • Des entrées supplémentaires peuvent être utilisées ici, comme décrit dans la documentation xclim sous « identifier ».

  • « identifier » : Un nom personnalisé pour votre indicateur. Ce sera le nom retourné dans les résultats.

  • « module »: Nécessaire, mais peut être n’importe quoi. Pour éviter un écrasement accidentel des indicateurs xclim, il est préférable d’utiliser quelque chose de différent de : [« atmos », « land », « generic »].

Pour une analyse des impacts des changements climatiques, le processus typique pour calculer les indicateurs non-fréquenciels serait de :

  1. Définir les indicateurs soit par les fonctionnalités xclim présentées ci-dessous ou par un fichier YAML.

  2. Appeler xhydro.indicators.compute_indicators, qui produira des résultats annuels par le biais d’un dictionnaire, où chaque clé représente les fréquences demandées.

  3. Appeler xhydro.cc.climatological_op pour chaque entrée du dictionnaire afin de calculer la moyenne sur 30 ans.

  4. Recombiner les jeux de données.

Cependant, si les résultats annuels ne sont pas nécessaires, xhydro.cc.produce_horizon peut contourner les étapes 2 à 4 et éviter beaucoup de tracas. Il le fait en supprimant l’axe time et en le remplaçant par une dimension horizon qui représente une tranche de temps. Dans le cas des indicateurs saisonniers ou mensuels, une dimension correspondante season ou month est également ajoutée.

Nous allons calculer le débit moyen estival (un indicateur annuel) et les débits moyens mensuels.

[27]:
indicators = [
    # Mean summer flow
    xclim.core.indicator.Indicator.from_dict(
        data={
            "base": "stats",
            "input": {"da": "streamflow"},
            "parameters": {"op": "mean", "indexer": {"month": [6, 7, 8]}},
        },
        identifier="qmoy_summer",
        module="hydro",
    ),
    # Mean monthly flow
    xclim.core.indicator.Indicator.from_dict(
        data={
            "base": "stats",
            "input": {"da": "streamflow"},
            "parameters": {"op": "mean", "freq": "MS"},
        },
        identifier="qmoy_monthly",
        module="hydro",
    ),
]

ds_indicators = xh.cc.produce_horizon(
    best_simulation, indicators=indicators, periods=["1991", "2020"]
)

ds_indicators
[27]:
<xarray.Dataset> Size: 312B
Dimensions:       (horizon: 1, month: 12)
Coordinates:
    basin_name    <U7 28B 'sub_001'
  * horizon       (horizon) <U9 36B '1991-2020'
  * month         (month) <U3 144B 'JAN' 'FEB' 'MAR' 'APR' ... 'OCT' 'NOV' 'DEC'
Data variables:
    qmoy_summer   (horizon) float64 8B 11.24
    qmoy_monthly  (horizon, month) float64 96B 7.05 5.431 14.18 ... 14.53 10.85
Attributes:
    cat:xrfreq:            fx
    cat:frequency:         fx
    cat:processing_level:  horizons
    cat:variable:          ('qmoy_monthly',)

Analyse fréquencielle

INFO

Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse fréquencielle locale.

Une analyse fréquencielle suit généralement ces étapes :

  1. Obtenir les données brutes nécessaires à l’analyse, telles que les maximums annuels, via xhydro.indicators.get_yearly_op.

  2. Appeler xhfa.local.fit pour obtenir les paramètres d’un nombre spécifié de distributions, telles que Gumbel, GEV et Pearson-III.

  3. (Facultatif) Appeler xhfa.local.criteria pour obtenir les paramètres de qualité d’ajustement.

  4. Appeler xhfa.local.parametric_quantiles pour obtenir les niveaux de retour.

Nous allons calculer les maximums annuels sur 20 et 100 ans, ainsi que le minimum des débits estivaux sur 7 jours pour une période de 2 ans.

[28]:
qref_max = xh.indicators.get_yearly_op(
    best_simulation,
    op="max",
    timeargs={"annual": {}},
    missing="pct",
    missing_options={"tolerance": 0.15},
)
qref_min = xh.indicators.get_yearly_op(
    best_simulation,
    op="min",
    window=7,
    timeargs={"summer": {"date_bounds": ["05-01", "11-30"]}},
    missing="pct",
    missing_options={"tolerance": 0.15},
)
[29]:
ax = plt.figure(figsize=(10, 5))
qref_max.streamflow_max_annual.dropna("time", how="all").plot()
qref_min.streamflow7_min_summer.dropna("time", how="all").plot()
[29]:
[<matplotlib.lines.Line2D at 0x7fa31436f290>]
../_images/notebooks_use_case_46_1.png
[30]:
params_max = xhfa.local.fit(
    qref_max,
    distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
    method="MLE",
    min_years=20,
).compute()
params_min = xhfa.local.fit(
    qref_min,
    distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
    method="MLE",
    min_years=20,
).compute()

params_max
[30]:
<xarray.Dataset> Size: 432B
Dimensions:                (scipy_dist: 4, dparams: 4)
Coordinates:
    horizon                <U9 36B '1991-2020'
  * dparams                (dparams) <U5 80B 'c' 'skew' 'loc' 'scale'
  * scipy_dist             (scipy_dist) <U10 160B 'genextreme' ... 'pearson3'
    basin_name             <U7 28B 'sub_001'
Data variables:
    streamflow_max_annual  (scipy_dist, dparams) float64 128B 0.01648 ... 98.41
Attributes:
    cat:xrfreq:            YS-JAN
    cat:frequency:         yr
    cat:processing_level:  indicators
    cat:variable:          ('streamflow_max_annual',)
    cat:id:                

Bien que non infaillible, le meilleur ajustement peut être identifié en utilisant le Critère d’Information Bayésien.

[31]:
criteria_max = xhfa.local.criteria(qref_max, params_max)
criteria_max = str(
    criteria_max.isel(
        scipy_dist=criteria_max.streamflow_max_annual.sel(criterion="bic")
        .argmin()
        .values
    ).scipy_dist.values
)  # Get the best fit as a string
print(f"Best distribution for the annual maxima: {criteria_max}")

criteria_min = xhfa.local.criteria(qref_min, params_min)
criteria_min = str(
    criteria_min.isel(
        scipy_dist=criteria_min.streamflow7_min_summer.sel(criterion="bic")
        .argmin()
        .values
    ).scipy_dist.values
)  # Get the best fit as a string
print(f"Best distribution for the summer 7-day minima: {criteria_min}")
Best distribution for the annual maxima: pearson3
Best distribution for the summer 7-day minima: genextreme

Les fonctions de traçage viendront éventuellement à xhydro.frequency_analysis, mais elles sont actuellement en cours de développement et sont cachées par défaut. Jusqu’à ce qu’une fonction publique soit ajoutée à la libraries, ces fonctions cachées peuvent toujours être appelées pour illustrer les résultats.

[32]:
# Plotting
data_max = xhfa.local._prepare_plots(
    params_max, xmin=1, xmax=1000, npoints=50, log=True
)
data_max["streamflow_max_annual"].attrs["long_name"] = "streamflow"
pp = xhfa.local._get_plotting_positions(qref_max[["streamflow_max_annual"]])

ax = plt.figure(figsize=(10, 5))
data_max.streamflow_max_annual.sel(scipy_dist=criteria_max).plot(color="k")
plt.xscale("log")
pp.plot.scatter(
    x="streamflow_max_annual_pp",
    y="streamflow_max_annual",
    color="k",
)
[32]:
<matplotlib.collections.PathCollection at 0x7fa3572e6ae0>
../_images/notebooks_use_case_51_1.png
[33]:
# Plotting
data_min = xhfa.local._prepare_plots(
    params_min, xmin=1, xmax=1000, npoints=50, log=True
)
data_min["streamflow7_min_summer"].attrs["long_name"] = "streamflow"
pp = xhfa.local._get_plotting_positions(qref_min[["streamflow7_min_summer"]])

ax = plt.figure(figsize=(10, 5))
data_min.streamflow7_min_summer.sel(scipy_dist=criteria_min).plot(color="k")
plt.xscale("log")
pp.plot.scatter(
    x="streamflow7_min_summer_pp",
    y="streamflow7_min_summer",
    color="k",
)
[33]:
<matplotlib.collections.PathCollection at 0x7fa3575d9970>
../_images/notebooks_use_case_52_1.png
[34]:
# Computation of return levels
rl_max = xhfa.local.parametric_quantiles(
    params_max.sel(scipy_dist=criteria_max).expand_dims("scipy_dist"), t=[20, 100]
).squeeze()
rl_min = xhfa.local.parametric_quantiles(
    params_min.sel(scipy_dist=criteria_min).expand_dims("scipy_dist"), t=[2], mode="min"
).squeeze()
rl_max
[34]:
<xarray.Dataset> Size: 144B
Dimensions:                (return_period: 2)
Coordinates:
    horizon                <U9 36B '1991-2020'
    scipy_dist             <U8 32B 'pearson3'
    basin_name             <U7 28B 'sub_001'
  * return_period          (return_period) int64 16B 20 100
    p_quantile             (return_period) float64 16B 0.95 0.99
Data variables:
    streamflow_max_annual  (return_period) float64 16B 418.2 536.4
Attributes:
    cat:xrfreq:            YS-JAN
    cat:frequency:         yr
    cat:processing_level:  indicators
    cat:variable:          ('streamflow_max_annual',)
    cat:id:                
[35]:
print(
    f"20-year annual maximum: {np.round(rl_max.streamflow_max_annual.sel(return_period=20).values, 1)} m³/s"
)
print(
    f"100-year annual maximum: {np.round(rl_max.streamflow_max_annual.sel(return_period=100).values, 1)} m³/s"
)
print(
    f"2-year minimum 7-day summer flow: {np.round(rl_min.streamflow7_min_summer.values, 1)} m³/s"
)
20-year annual maximum: 418.2 m³/s
100-year annual maximum: 536.4 m³/s
2-year minimum 7-day summer flow: 2.8 m³/s

Simulations et indicateurs de débit futur

Données météorologiques futures

Maintenant que nous avons accès à un modèle hydrologique calé et à des indicateurs historiques, nous pouvons réaliser l’analyse d’impact des changements climatiques. Cet exemple utilisera un ensemble de modèles CMIP6 qui ont été débiaisés en utilisant ERA5-Land, pour assurer la cohérence avec le produit de référence. Plus précisément, nous utiliserons le jeu de données ESPO-G6-E5L, également hébergé sur PAVICS. Bien qu’il soit recommandé d’utiliser plusieurs scénarios d’émission, cet exemple se limitera à utiliser les simulations SSP2-4.5 provenant de 14 modèles climatiques.

Nous pouvons généralement réutiliser le même code que ci-dessus. Une différence est que les modèles climatiques utilisent souvent des calendriers personnalisés qui doivent être convertis en calendriers standard. Encore une fois, xscen peut être utilisé pour cela.

[36]:
models = [
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CAS_FGOALS-g3_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CMCC_CMCC-ESM2_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CSIRO-ARCCSS_ACCESS-CM2_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CSIRO_ACCESS-ESM1-5_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_DKRZ_MPI-ESM1-2-HR_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_INM_INM-CM5-0_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MIROC_MIROC6_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MPI-M_MPI-ESM1-2-LR_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MRI_MRI-ESM2-0_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NCC_NorESM2-LM_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CNRM-CERFACS_CNRM-ESM2-1_ssp245_r1i1p1f2_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NIMS-KMA_KACE-1-0-G_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NOAA-GFDL_GFDL-ESM4_ssp245_r1i1p1f1_1950-2100.ncml",
    "day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_BCC_BCC-CSM2-MR_ssp245_r1i1p1f1_1950-2100.ncml",
]

Le code ci-dessous montrera comment procéder avec la première simulation, mais il pourrait être utilisé dans une boucle pour traiter les 14 simulations.

[37]:
model = models[0]

meteo_sim = xr.open_dataset(
    f"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/datasets/simulations/bias_adjusted/cmip6/ouranos/ESPO-G/ESPO-G6-E5Lv1.0.0/{model}",
    engine="netcdf4",
    chunks={"time": 365, "lon": 50, "lat": 50},
)
meteo_sim = xscen.spatial.subset(meteo_sim, method="shape", tile_buffer=2, shape=gdf)
meteo_sim = meteo_sim.assign_coords({"crs": meteo_sim.crs})
meteo_sim = xscen.spatial_mean(
    meteo_sim,
    method="xesmf",
    region={"method": "shape", "shape": gdf},
    simplify_tolerance=0.1,
)
for c in meteo_sim.coords:
    if len(meteo_sim[c].dims) == 0:
        if c not in ["lon", "lat"]:
            meteo_sim = meteo_sim.drop_vars([c])
        else:
            meteo_sim[c] = meteo_sim[c].expand_dims("HYBAS_ID")

meteo_sim = xscen.utils.change_units(
    meteo_sim, {"tasmax": "degC", "tasmin": "degC", "pr": "mm"}
)

# Manage calendars
meteo_sim = xscen.utils.clean_up(
    meteo_sim,
    convert_calendar_kwargs={"calendar": "standard", "align_on": "random"},
    missing_by_var={"tasmin": "interpolate", "tasmax": "interpolate", "pr": 0},
)

meteo_sim.to_netcdf(notebook_folder / "_data" / f"meteo_sim0.nc")

Données de débit et indicateurs futurs

Encore une fois, le même code que précédemment peut être réutilisé ici, mais avec xhydro.modelling.hydrological_model. La principale différence est que les meilleurs paramètres peuvent être utilisés lors de la configuration du modèle hydrologique, et que les dates couvrent toute la période allant jusqu’en 2100.

Les indicateurs sont également calculés de manière similaire, avec l’ajout de l’utilisation d’une liste de périodes dans l’argument periods pour créer une dimension horizon, au lieu d’une période unique. La fonction xhydro.frequency_analysis.fit peut également accepter un argument periods.

Le code ci-dessous ne montrera encore qu’une seule simulation, mais pourrait être utilisé pour traiter les 14 simulations.

[38]:
from copy import deepcopy

import xhydro.modelling as xhm
[39]:
model_cfg = deepcopy(model_config)

model_cfg["meteo_file"] = notebook_folder / "_data" / f"meteo_sim0.nc"
model_cfg["parameters"] = np.array(best_parameters)
model_cfg["start_date"] = "1991-01-01"
model_cfg["end_date"] = "2099-12-31"

qsim = xhm.hydrological_model(model_cfg).run()
[40]:
plt.figure(figsize=(10, 5))
qsim.streamflow.plot()
[40]:
[<matplotlib.lines.Line2D at 0x7fa3573e0470>]
../_images/notebooks_use_case_62_1.png
[41]:
# Non-frequential indicators
ds_indicators = xh.cc.produce_horizon(
    qsim, indicators=indicators, periods=[["1991", "2020"], ["2070", "2099"]]
)

# Frequency analysis
qsim_max = xh.indicators.get_yearly_op(
    qsim,
    op="max",
    timeargs={"annual": {}},
    missing="pct",
    missing_options={"tolerance": 0.15},
)
qsim_min = xh.indicators.get_yearly_op(
    qsim,
    op="min",
    window=7,
    timeargs={"summer": {"date_bounds": ["05-01", "11-30"]}},
    missing="pct",
    missing_options={"tolerance": 0.15},
)

params_max = xhfa.local.fit(
    qsim_max,
    distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
    method="MLE",
    min_years=20,
    periods=[["1991", "2020"], ["2070", "2099"]],
).compute()
params_min = xhfa.local.fit(
    qsim_min,
    distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
    method="MLE",
    min_years=20,
    periods=[["1991", "2020"], ["2070", "2099"]],
).compute()

criteria_max = xhfa.local.criteria(
    qsim_max.sel(time=slice("1991", "2020")), params_max.sel(horizon="1991-2020")
)
criteria_max = str(
    criteria_max.isel(
        scipy_dist=criteria_max.streamflow_max_annual.sel(criterion="bic")
        .argmin()
        .values
    ).scipy_dist.values
)  # Get the best fit as a string

criteria_min = xhfa.local.criteria(
    qsim_min.sel(time=slice("1991", "2020")), params_min.sel(horizon="1991-2020")
)
criteria_min = str(
    criteria_min.isel(
        scipy_dist=criteria_min.streamflow7_min_summer.sel(criterion="bic")
        .argmin()
        .values
    ).scipy_dist.values
)  # Get the best fit as a string

# Computation of return levels
rl_max = xhfa.local.parametric_quantiles(
    params_max.sel(scipy_dist=criteria_max).expand_dims("scipy_dist"), t=[20, 100]
).squeeze()
rl_min = xhfa.local.parametric_quantiles(
    params_min.sel(scipy_dist=criteria_min).expand_dims("scipy_dist"),
    t=[2],
    mode="min",
).squeeze()

# Combine all indicators
ds_indicators = xr.merge([ds_indicators, rl_max, rl_min], compat="override")

ds_indicators
[41]:
<xarray.Dataset> Size: 564B
Dimensions:                 (horizon: 2, month: 12, return_period: 2)
Coordinates:
    basin_name              <U7 28B 'sub_001'
  * horizon                 (horizon) <U9 72B '1991-2020' '2070-2099'
  * month                   (month) <U3 144B 'JAN' 'FEB' 'MAR' ... 'NOV' 'DEC'
    scipy_dist              <U8 32B 'pearson3'
  * return_period           (return_period) int64 16B 20 100
    p_quantile              (return_period) float64 16B 0.95 0.99
Data variables:
    qmoy_summer             (horizon) float64 16B 11.34 10.04
    qmoy_monthly            (horizon, month) float64 192B 8.837 7.288 ... 13.95
    streamflow_max_annual   (return_period, horizon) float64 32B 371.0 ... 560.9
    streamflow7_min_summer  (horizon) float64 16B 2.412 2.402
Attributes:
    cat:xrfreq:            fx
    cat:frequency:         fx
    cat:processing_level:  horizons
    cat:variable:          ('qmoy_monthly',)

Impacts des changements climatiques

INFO

Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse de l’impact des changements climatiques.

Cet exemple gardera l’analyse de l’impact des changements climatiques relativement simple.

  • Calculez la différence entre les périodes future et de référence en utilisant xhydro.cc.compute_deltas.

  • Utilisez ces différences pour calculer les statistiques d’ensemble en utilisant xhydro.cc.ensemble_stats : percentiles de l’ensemble et accord entre les modèles climatiques.

[42]:
# Differences
deltas = xh.cc.compute_deltas(
    ds_indicators, reference_horizon="1991-2020", kind="%", rename_variables=False
).isel(horizon=-1)

# Save the results
deltas.to_netcdf(notebook_folder / "_data" / f"deltas_sim0.nc")

deltas.squeeze()
[42]:
<xarray.Dataset> Size: 400B
Dimensions:                 (month: 12, return_period: 2)
Coordinates:
    basin_name              <U7 28B 'sub_001'
    horizon                 <U9 36B '2070-2099'
  * month                   (month) <U3 144B 'JAN' 'FEB' 'MAR' ... 'NOV' 'DEC'
    scipy_dist              <U8 32B 'pearson3'
  * return_period           (return_period) int64 16B 20 100
    p_quantile              (return_period) float64 16B 0.95 0.99
Data variables:
    qmoy_summer             float64 8B -11.47
    qmoy_monthly            (month) float64 96B 13.49 11.94 61.1 ... 22.05 24.16
    streamflow_max_annual   (return_period) float64 16B 15.6 25.8
    streamflow7_min_summer  float64 8B -0.4241
Attributes:
    cat:xrfreq:            fx
    cat:frequency:         fx
    cat:processing_level:  deltas
    cat:variable:          ('qmoy_monthly',)

Il existe de nombreuses façons de créer l’ensemble lui-même. Si vous utilisez un dictionnaire de jeux de données, la clé sera utilisée pour nommer chaque élément de la nouvelle dimension realization. Cela peut être très utile lorsque l’on effectue des analyses plus détaillées ou lorsque l’on souhaite pondérer les différents modèles en fonction, par exemple, du nombre de simulations disponibles. Dans notre cas, puisque nous souhaitons seulement calculer des statistiques d’ensemble, nous pouvons simplifier et fournir simplement une liste.

[43]:
import pooch

# Acquire deltas for the other 13 simulations
from xhydro.testing.helpers import (  # In-house function to access xhydro-testdata
    deveraux,
)

deltas_files = deveraux().fetch("use_case/deltas.zip", processor=pooch.Unzip())
deltas_files.extend([notebook_folder / "_data" / f"deltas_sim0.nc"])
[44]:
# Statistics to compute
statistics = {
    "ensemble_percentiles": {"values": [10, 25, 50, 75, 90], "split": False},
    "robustness_fractions": {"test": None},
}

ens_stats = xh.cc.ensemble_stats(deltas_files, statistics)

ens_stats
[44]:
<xarray.Dataset> Size: 2kB
Dimensions:                                  (percentiles: 5, month: 12,
                                              return_period: 2)
Coordinates:
    basin_name                               <U7 28B 'sub_001'
    horizon                                  <U9 36B '2070-2099'
  * percentiles                              (percentiles) int64 40B 10 ... 90
  * month                                    (month) <U3 144B 'JAN' ... 'DEC'
  * return_period                            (return_period) int64 16B 20 100
    p_quantile                               (return_period) float64 16B 0.95...
Data variables: (12/32)
    qmoy_summer                              (percentiles) float64 40B -14.0 ...
    qmoy_monthly                             (month, percentiles) float64 480B dask.array<chunksize=(12, 5), meta=np.ndarray>
    streamflow_max_annual                    (return_period, percentiles) float64 80B dask.array<chunksize=(2, 5), meta=np.ndarray>
    streamflow7_min_summer                   (percentiles) float64 40B -28.54...
    qmoy_summer_changed                      float64 8B 1.0
    qmoy_summer_positive                     float64 8B 0.5714
    ...                                       ...
    streamflow7_min_summer_positive          float64 8B 0.2143
    streamflow7_min_summer_changed_positive  float64 8B 0.2143
    streamflow7_min_summer_negative          float64 8B 0.7857
    streamflow7_min_summer_changed_negative  float64 8B 0.7857
    streamflow7_min_summer_valid             float64 8B 1.0
    streamflow7_min_summer_agree             float64 8B 0.7857
Attributes:
    cat:xrfreq:            fx
    cat:frequency:         fx
    cat:processing_level:  ensemble
    cat:variable:          qmoy_monthly
    cat:id:
    ensemble_size:         14
[45]:
# Recreate the boxplots based on the computed percentiles
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(13, 5), sharey=True)

ax = plt.subplot(1, 3, 1)
for i, rp in enumerate(ens_stats.return_period.values):
    stats = [
        {
            "label": rp,
            "med": ens_stats.streamflow_max_annual.sel(
                percentiles=50, return_period=rp
            ).values,
            "q1": ens_stats.streamflow_max_annual.sel(
                percentiles=25, return_period=rp
            ).values,
            "q3": ens_stats.streamflow_max_annual.sel(
                percentiles=75, return_period=rp
            ).values,
            "whislo": ens_stats.streamflow_max_annual.sel(
                percentiles=10, return_period=rp
            ).values,
            "whishi": ens_stats.streamflow_max_annual.sel(
                percentiles=90, return_period=rp
            ).values,
        }
    ]

    ax.bxp(stats, showfliers=False, positions=[i], widths=0.5)
ax.set_title("Maximum annual streamflow")
plt.xlabel("Return period")
plt.ylabel("Difference Fut-Hist (%)")

ax = plt.subplot(1, 3, 2)
for i, rp in enumerate(ens_stats.return_period.values):
    stats = [
        {
            "label": rp,
            "med": ens_stats.streamflow7_min_summer.sel(percentiles=50).values,
            "q1": ens_stats.streamflow7_min_summer.sel(percentiles=25).values,
            "q3": ens_stats.streamflow7_min_summer.sel(percentiles=75).values,
            "whislo": ens_stats.streamflow7_min_summer.sel(percentiles=10).values,
            "whishi": ens_stats.streamflow7_min_summer.sel(percentiles=90).values,
        }
    ]

    ax.bxp(stats, showfliers=False, positions=[i], widths=0.5)
ax.set_title("Minimum summer streamflow (7-day avg)")
plt.xlabel("")

ax = plt.subplot(1, 3, 3)
stats = [
    {
        "label": "",
        "med": ens_stats.qmoy_summer.sel(percentiles=50).values,
        "q1": ens_stats.qmoy_summer.sel(percentiles=25).values,
        "q3": ens_stats.qmoy_summer.sel(percentiles=75).values,
        "whislo": ens_stats.qmoy_summer.sel(percentiles=10).values,
        "whishi": ens_stats.qmoy_summer.sel(percentiles=90).values,
    }
]

ax.bxp(stats, showfliers=False, positions=[i], widths=0.25)
ax.set_title("Mean summer flow")

plt.show()
../_images/notebooks_use_case_69_0.png
[46]:
print(
    f"Fraction of simulations with a positive change (maximum streamflow): {ens_stats.streamflow_max_annual_positive.values}"
)
print(
    f"Fraction of simulations with a positive change (minimum summer streamflow): {ens_stats.streamflow7_min_summer_positive.values}"
)
print(
    f"Fraction of simulations with a positive change (mean summer streamflow): {ens_stats.qmoy_summer_positive.values}"
)
Fraction of simulations with a positive change (maximum streamflow): [0.5        0.64285714]
Fraction of simulations with a positive change (minimum summer streamflow): 0.21428571428571427
Fraction of simulations with a positive change (mean summer streamflow): 0.5714285714285714