smashbox.plot.plot

   1# -*- coding: utf-8 -*-
   2import matplotlib.pyplot as plt
   3import numpy as np
   4import matplotlib
   5from matplotlib.colors import ListedColormap
   6import pandas as pd
   7from pandas import DataFrame
   8from smashbox.stats import stats
   9from smashbox.tools import geo_toolbox
  10import datetime
  11from smash import Model
  12
  13import matplotlib.colors as mcolors
  14from matplotlib import cm
  15import colorsys
  16from mpl_toolkits.axes_grid1 import make_axes_locatable
  17
  18import os
  19import pandas as pd
  20from smashbox.init.param import param
  21
  22from smashbox.tools import tools
  23
  24
  25class plot_properties:
  26    """Class which handle differents properties of the matplotlib plot function.
  27    All attributes can be defined by the user and the object plot_properties can be
  28     passed to any smashbox plot function.
  29    """
  30
  31    def __init__(
  32        self,
  33        ls="-",
  34        lw=1.5,
  35        marker="",
  36        markersize=4,
  37        color="black",
  38        label="",
  39    ):
  40        self.ls = ls
  41        """The style of the line (see matplotlib documentation)"""
  42        self.lw = lw
  43        """The linewidth of the line (see matplotlib documentation)"""
  44        self.marker = marker
  45        """The style of the marker (see matplotlib documentation)"""
  46        self.markersize = markersize
  47        """The size of the marker (see matplotlib documentation)"""
  48        self.color = color
  49        """The color of the line (see matplotlib documentation)"""
  50        self.label = label
  51        """The label of the line (see matplotlib documentation)"""
  52
  53    def update(self, **kwargs):
  54        """Update the class attributes using kwarg (dictionnary)"""
  55        for key, values in kwargs.items():
  56            setattr(self, key, values)
  57
  58
  59class ax_properties:
  60    """Class which handle differents properties of the matplotlib ax object
  61    (see matplotlib documentation). All attributes can be defined by the user and the
  62     object ax_properties can be passed to any smashbox plot function.
  63    """
  64
  65    def __init__(
  66        self,
  67        title: str = None,
  68        xlabel: str = None,
  69        ylabel: str = None,
  70        clabel: str = None,
  71        font_ratio: int = 1,
  72        title_fontsize: int = 12,
  73        label_fontsize: int = 10,
  74        annotate_fontsize: int = 6,
  75        grid: bool = True,
  76        xscale: str | None = None,
  77        yscale: str | None = None,
  78        legend: bool = True,
  79        legend_loc: str = None,
  80        legend_fontsize: int = 8,
  81        xtics_fontsize: int = 8,
  82        ytics_fontsize: int = 8,
  83        cmap: str | None = None,
  84        xlim: tuple | list | None = (None, None),
  85        ylim: tuple | list | None = (None, None),
  86        xticklabels_rotation: int = 0,
  87        barlabel_fontsize=6,
  88    ):
  89
  90        self.title = title
  91        """The title of the graphic"""
  92        self.xlabel = xlabel
  93        """The label of the x axis"""
  94        self.ylabel = ylabel
  95        """The label of the y axis"""
  96        self.clabel = clabel
  97        """The label of the colorbar"""
  98        self.font_ratio = font_ratio
  99        """Ratio of the global fontsize"""
 100        self.title_fontsize = title_fontsize
 101        """The fontsize of the title"""
 102        self.label_fontsize = label_fontsize
 103        """The label fontsize"""
 104        self.annotate_fontsize = annotate_fontsize
 105        """The annotation fontsize in plot"""
 106        self.grid = grid
 107        """Set the grid (boolean), default True"""
 108        self.xscale = xscale
 109        """Scale of the x axis (see matplotlib documentation)"""
 110        self.yscale = yscale
 111        """Scale of the x axis (see matplotlib documentation)"""
 112        self.legend = legend
 113        """Set the legend, boolean, default is True"""
 114        self.legend_loc = legend_loc
 115        """Localisation of the legend"""
 116        self.legend_fontsize = legend_fontsize
 117        """The fontsize of the legend"""
 118        self.xtics_fontsize = xtics_fontsize
 119        """The fontsize of the xtics"""
 120        self.ytics_fontsize = ytics_fontsize
 121        """The fontsize of the ytics"""
 122        self.cmap = cmap
 123        """The name of the used colormap"""
 124        self.xlim = xlim
 125        """The limit of the x axis, tuple or list, default is (None,None)"""
 126        self.ylim = ylim
 127        """The limit of the y axis, tuple or list, default is (None,None)"""
 128        self.xticklabels_rotation = xticklabels_rotation
 129        """Angle of the xtics labels, float, default is 0."""
 130        self.barlabel_fontsize = barlabel_fontsize * self.font_ratio
 131        "Fontsize of the bar top label"
 132
 133    def update(self, **kwargs):
 134        """Update the class attributes using kwarg (dictionnary)"""
 135        for key, values in kwargs.items():
 136            setattr(self, key, values)
 137
 138    def change(self, figure):
 139        """Apply change to the current figure `figure`
 140        Parameter:
 141        ----------
 142        figure : list or tuple
 143            list of (fig, ax), figure and ax of a matplotlib subplot.
 144        Return:
 145        -------
 146            a tuple of the modified (fig,ax)
 147        """
 148        fig, ax = figure
 149
 150        plt.rcParams.update(plt.rcParamsDefault)
 151
 152        plt.rcParams.update(
 153            {
 154                "axes.labelsize": self.label_fontsize * self.font_ratio,
 155                "axes.titlesize": self.title_fontsize * self.font_ratio,
 156                "legend.fontsize": self.legend_fontsize * self.font_ratio,
 157                "figure.titlesize": self.title_fontsize * self.font_ratio,
 158                "xtick.labelsize": self.xtics_fontsize * self.font_ratio,
 159                "ytick.labelsize": self.ytics_fontsize * self.font_ratio,
 160            }
 161        )
 162
 163        if self.title is not None:
 164
 165            ax.set_title(
 166                self.title, fontsize=self.title_fontsize * self.font_ratio
 167            )
 168
 169        if self.xlabel is not None:
 170            ax.set_xlabel(
 171                self.xlabel, fontsize=self.label_fontsize * self.font_ratio
 172            )
 173
 174        if self.ylabel is not None:
 175            ax.set_ylabel(
 176                self.ylabel, fontsize=self.label_fontsize * self.font_ratio
 177            )
 178
 179        if self.grid:
 180            ax.grid(True, which="both", linestyle="--", alpha=0.5)
 181
 182        if self.xscale is not None:
 183            ax.set_xscale(self.xscale)
 184
 185        if self.yscale is not None:
 186            ax.set_xyscale(self.yscale)
 187
 188        if self.legend:
 189            ax.legend(
 190                loc=self.legend_loc,
 191                fontsize=self.legend_fontsize * self.font_ratio,
 192            )
 193
 194        if self.cmap is not None:
 195            plt.rc("image", cmap=self.cmap)
 196
 197        if self.ylim[0] is not None:
 198            ax.set_ylim(bottom=self.ylim[0])
 199
 200        if self.ylim[1] is not None:
 201            ax.set_ylim(top=self.ylim[1])
 202
 203        if self.xlim[0] is not None:
 204            ax.set_xlim(left=self.xlim[0])
 205
 206        if self.xlim[1] is not None:
 207            ax.set_xlim(right=self.xlim[1])
 208
 209        if self.xticklabels_rotation > 0:
 210            ax.set_xticklabels(
 211                ax.get_xticklabels(),
 212                rotation=self.xticklabels_rotation,
 213                ha="right",
 214            )
 215
 216        return fig, ax
 217
 218
 219class fig_properties:
 220    """Class which handle differents properties of the matplotlib fig object
 221    (see matplotlib documentation). All attributes can be defined by the user and the
 222     object ax_properties can be passed to any smashbox plot function.
 223    """
 224
 225    def __init__(
 226        self,
 227        figname=None,
 228        xsize=8,
 229        ysize=6,
 230        transparent=False,
 231        dpi=160,
 232        font_ratio=1,
 233        bbox_inches="tight",
 234    ):
 235
 236        self.figname = figname
 237        """Path to the figure name to be saved"""
 238        self.xsize = xsize
 239        """Width of the figure in inch"""
 240        self.ysize = ysize
 241        """Height of the figure in inch"""
 242        self.transparent = transparent
 243        """Use transparency when exporting the figure, default is False"""
 244        self.dpi = dpi
 245        """RƩsolution (dpi), int, default is 80"""
 246        self.font_ratio = font_ratio
 247        """Global font ratio"""
 248        self.bbox_inches = bbox_inches
 249        """Constraint of the boundingbox of each ax (see matplotlib docuentation)"""
 250
 251    def update(self, **kwargs):
 252        """Update the class attributes using kwarg (dictionnary)"""
 253
 254        for key, values in kwargs.items():
 255            setattr(self, key, values)
 256
 257    def change(self, figure):
 258        """Apply change to the current figure `figure`
 259        Parameter:
 260        ----------
 261        figure : list or tuple
 262            list of (fig, ax), figure and ax of a matplotlib subplot.
 263        Return:
 264        -------
 265            a tuple of the modified (fig,ax)
 266        """
 267
 268        fig, ax = figure
 269
 270        fig.set_figheight(self.ysize)
 271        fig.set_figwidth(self.xsize)
 272
 273        plt.rc(
 274            "font", size=plt.rcParams["font.size"] * self.font_ratio
 275        )  # controls default text sizes
 276        plt.rc(
 277            "axes", titlesize=plt.rcParams["axes.titlesize"] * self.font_ratio
 278        )  # fontsize of the axes title
 279        plt.rc(
 280            "axes", labelsize=plt.rcParams["axes.labelsize"] * self.font_ratio
 281        )  # fontsize of the x and y labels
 282        plt.rc(
 283            "xtick",
 284            labelsize=plt.rcParams["xtick.labelsize"] * self.font_ratio,
 285        )  # fontsize of the tick labels
 286        plt.rc(
 287            "ytick",
 288            labelsize=plt.rcParams["ytick.labelsize"] * self.font_ratio,
 289        )  # fontsize of the tick labels
 290        plt.rc(
 291            "legend",
 292            fontsize=plt.rcParams["legend.fontsize"] * self.font_ratio,
 293        )  # legend fontsize
 294        plt.rc(
 295            "figure",
 296            titlesize=plt.rcParams["figure.titlesize"] * self.font_ratio,
 297        )  # fontsize of the figure title
 298
 299        if self.figname is not None:
 300
 301            head_path, basename = os.path.split(self.figname)
 302
 303            if len(head_path) > 0 and not os.path.exists(head_path):
 304                os.makedirs(head_path)
 305
 306            fig.savefig(
 307                self.figname,
 308                transparent=self.transparent,
 309                dpi=self.dpi,
 310                bbox_inches=self.bbox_inches,
 311            )
 312
 313        return fig, ax
 314
 315
 316@tools.autocast_args
 317def save_figure(
 318    fig=None, figname="myfigure", xsize=8, ysize=6, transparent=False, dpi=80
 319):
 320    """
 321    Save a figure.
 322    Parameters:
 323    -----------
 324    fig: fig object returned by matplotlib.subplot
 325        the figure to save
 326    figname: str
 327        Path to the figure
 328    xsize: int
 329        width of the figure in inch
 330    ysize: int
 331        height of the figure in inch
 332    transparent: bool, default is False
 333        use transparency
 334    dpi : int
 335        resolution of the figure, default is 80
 336    """
 337    fig.set_size_inches(xsize, ysize, forward=True)
 338    fig.savefig(figname, transparent=transparent, dpi=dpi, bbox_inches="tight")
 339
 340
 341def generate_palette(base_color, n, variation="hue"):
 342    """
 343    Generate a palette of colors from a base color.
 344    Parameter:
 345    ---------
 346    base_color: str
 347        matplotlib color string
 348    n: int
 349        number of color to generate
 350    variation: 'hue' | 'brightness'
 351        how to generate the color palette, by changing the hue or the brighness of the base color.
 352    Return: a list of colors
 353    """
 354    # Convertir la couleur de base en format RGB normalisƩ (0-1)
 355    rgb = mcolors.to_rgb(base_color)
 356
 357    # Convertir en HSV
 358    h, s, v = colorsys.rgb_to_hsv(*rgb)
 359
 360    # GƩnƩrer n couleurs en modifiant la teinte ou valeur
 361    palette = []
 362    for i in range(n):
 363        new_h = h
 364        if variation == "hue":
 365            new_h = (h + i / n) % 1.0  # cycle dans le cercle chromatique
 366        new_v = v
 367        if variation == "brightness":
 368            new_v = max(
 369                0.1, min(1.0, v * (0.5 + i / (2 * n)))
 370            )  # Ʃviter le noir complet
 371
 372        new_rgb = colorsys.hsv_to_rgb(new_h, s, new_v)
 373        palette.append(new_rgb)
 374
 375    return palette
 376
 377
 378@tools.autocast_args
 379def plot_chro(
 380    data: np.ndarray = np.zeros(shape=(1, 10)),
 381    t_axis: int = 1,
 382    outlets_name: list | tuple = [],
 383    columns: list | tuple = [],
 384    dt: float = 0.0,
 385    xtics: list | tuple = [],
 386    date_range: list = None,
 387    figure=None,
 388    ax_settings: dict | ax_properties = ax_properties(),
 389    fig_settings: dict | fig_properties = fig_properties(),
 390    plot_settings: dict | plot_properties = plot_properties(),
 391):
 392    """
 393    Plot a temporal chonic of values
 394    Parameters:
 395    -----------
 396    data: np.ndarray of dimenion 2.
 397        data to plot as a matrix of 2 dimension.
 398    t_axis : int
 399        the axis of the time in data, default is 1
 400    outlets_name: list
 401        the list of the outlets name
 402    columns : list
 403        the column to be plotted in t_axis direction
 404    dt: float
 405        the timestep
 406    xtics : list
 407        list of date for the xtics. The format must be automatically read by numpy.Datetime
 408    date_range: list
 409        list of [date_start, date_end, timedelta] to generate the xtics
 410    figure: tuple
 411        input figure as (fig,ax) to add a new curve
 412    ax_settings: dict or class ax_properties
 413        object or dict with any attribute of class ax_properties
 414    fig_settings: dict or class ax_properties
 415        object or dict with any attribute of class fig_settings
 416    plot_settings: dict or class plot_properties
 417        object or dict with any attribute of class plot_settings
 418
 419    """
 420
 421    if isinstance(ax_settings, dict):
 422        ax_settings = ax_properties(**ax_settings)
 423    else:
 424        ax_settings = ax_properties(**ax_settings.__dict__)
 425
 426    if isinstance(fig_settings, dict):
 427        fig_settings = fig_properties(**fig_settings)
 428    else:
 429        fig_settings = fig_properties(**fig_settings.__dict__)
 430
 431    if isinstance(plot_settings, dict):
 432        plot_settings = plot_properties(**plot_settings)
 433    else:
 434        plot_settings = plot_properties(**plot_settings.__dict__)
 435
 436    data = np.moveaxis(data, t_axis, 0)
 437
 438    if figure is not None:
 439        fig, ax = figure
 440    else:
 441        fig, ax = plt.subplots()
 442
 443    fig, ax = ax_settings.change(figure=(fig, ax))
 444
 445    if len(xtics) == 0:
 446        xtics = np.arange(0, data.shape[0])
 447        if dt > 0:
 448            xtics = xtics * dt
 449    else:
 450        for i in range(len(xtics)):
 451            xtics[i] = np.datetime64(xtics[i])
 452
 453    if date_range is not None:
 454        if len(date_range) != 3:
 455            raise ValueError(
 456                "date_range must have a length of 3: [date_start, date_end, step (s)]"
 457            )
 458        xtics = np.arange(
 459            np.datetime64(date_range[0]),
 460            np.datetime64(
 461                date_range[1] + pd.Timedelta(seconds=int(date_range[2]))
 462            ),
 463            np.timedelta64(int(date_range[2]), "s"),
 464        )
 465
 466    if len(columns) > 0:
 467
 468        args = plot_settings.__dict__.copy()
 469        print(args)
 470        del args["label"]
 471        del args["color"]
 472
 473        palette = generate_palette(plot_settings.color, len(columns))
 474
 475        for i in columns:
 476            ax.plot(
 477                xtics[:],
 478                data[:, i],
 479                **args,
 480                label=outlets_name[i],
 481                color=palette[i],
 482            )
 483    else:
 484        ax.plot(xtics[:], data[:, 0], **plot_settings.__dict__)
 485
 486    fig, ax = ax_settings.change(figure=(fig, ax))
 487    fig, ax = fig_settings.change(figure=(fig, ax))
 488
 489    return fig, ax
 490
 491
 492def plot_hydrograph(
 493    model: Model | None = None,
 494    columns: list | tuple = [],
 495    outlets_name: list | tuple = [],
 496    plot_rainfall: bool = True,
 497    plot_qobs: bool = True,
 498    figure: list | tuple | None = None,
 499    ax_settings: dict | ax_properties = {},
 500    fig_settings: dict | fig_properties = {},
 501    plot_settings_sim: dict | plot_properties = {},
 502    plot_settings_obs: dict | plot_properties = {},
 503):
 504    """
 505    Plot an hydrograph from a smash model
 506    Parameters:
 507    -----------
 508    model: a smash model object
 509        a smash model object
 510    outlets_name: list
 511        the list of the outlets name
 512    columns : list
 513        the column to be plotted in t_axis direction
 514    figure: tuple
 515        input figure as (fig,ax) to add a new curve
 516    ax_settings: dict or class ax_properties
 517        object or dict with any attribute of class ax_properties
 518    fig_settings: dict or class ax_properties
 519        object or dict with any attribute of class fig_settings
 520    plot_settings_sim: dict or class ax_properties
 521        object or dict with any attribute of class plot_settings.Control the simulated curve
 522    plot_settings_obs: dict or class ax_properties
 523        object or dict with any attribute of class plot_settings. Control the observed curve
 524
 525    """
 526    if model is None:
 527        raise ValueError("Input smash model object is None.")
 528
 529    if isinstance(ax_settings, dict):
 530        default_ax_settings = ax_properties(
 531            xlabel="Time",
 532            ylabel="discharges m^3/s",
 533            xtics_fontsize=10,
 534            ytics_fontsize=10,
 535        )
 536        default_ax_settings.update(**ax_settings)
 537    else:
 538        default_ax_settings = ax_properties(**ax_settings.__dict__)
 539
 540    if isinstance(fig_settings, dict):
 541        fig_settings = fig_properties(**fig_settings)
 542    else:
 543        fig_settings = fig_properties(**fig_settings.__dict__)
 544
 545    if isinstance(plot_settings_sim, dict):
 546        default_plot_settings_sim = plot_properties(
 547            ls="-",
 548            lw="2",
 549            marker="",
 550            markersize=4,
 551            color="blue",
 552            label="Sim",
 553        )
 554        default_plot_settings_sim.update(**plot_settings_sim)
 555    else:
 556        default_plot_settings_sim = plot_properties(
 557            **plot_settings_sim.__dict__
 558        )
 559
 560    # default color for multi curves: same color but different line type
 561    if len(columns) >= 2:
 562        color = "blue"
 563    else:
 564        color = "black"
 565
 566    if isinstance(plot_settings_obs, dict):
 567        default_plot_settings_obs = plot_properties(
 568            ls="--",
 569            lw="1.5",
 570            marker="",
 571            markersize=4,
 572            color=color,
 573            label="Obs",
 574        )
 575        default_plot_settings_obs.update(**plot_settings_obs)
 576    else:
 577        default_plot_settings_obs = plot_properties()
 578
 579    # manage date here
 580    date_deb = datetime.datetime.fromisoformat(
 581        model.setup.start_time
 582    ) + datetime.timedelta(seconds=int(model.setup.dt))
 583    date_end = datetime.datetime.fromisoformat(model.setup.end_time)
 584    date_range = [date_deb, date_end, model.setup.dt]
 585
 586    if figure is None:
 587        if plot_rainfall:
 588            fig, (ax2, ax1) = plt.subplots(2, 1, height_ratios=[1, 4])
 589            fig.subplots_adjust(hspace=0)
 590            figure = [fig, ax1, ax2]
 591        else:
 592            fig, ax2 = plt.subplots()
 593            figure = [fig, ax1]
 594    else:
 595        if plot_rainfall:
 596            fig = figure[0]
 597            ax1 = figure[1]
 598            ax2 = figure[2]
 599        else:
 600            fig = figure[0]
 601            ax1 = figure[1]
 602
 603    fig, ax = default_ax_settings.change(figure=(fig, ax1))
 604
 605    if plot_qobs:
 606        fig, ax1 = plot_chro(
 607            np.where(model.response_data.q < 0, np.nan, model.response_data.q),
 608            date_range=date_range,
 609            columns=columns,
 610            outlets_name=["obs_" + name for name in outlets_name],
 611            figure=(fig, ax1),
 612            ax_settings=default_ax_settings,
 613            fig_settings=fig_settings,
 614            plot_settings=default_plot_settings_obs,
 615        )
 616
 617    fig, ax1 = plot_chro(
 618        model.response.q,
 619        date_range=date_range,
 620        columns=columns,
 621        outlets_name=["sim_" + name for name in outlets_name],
 622        figure=(fig, ax1),
 623        ax_settings=default_ax_settings,
 624        fig_settings=fig_settings,
 625        plot_settings=default_plot_settings_sim,
 626    )
 627
 628    xtics = np.arange(
 629        np.datetime64(date_range[0]),
 630        np.datetime64(
 631            date_range[1] + datetime.timedelta(seconds=int(date_range[2]))
 632        ),
 633        np.timedelta64(int(date_range[2]), "s"),
 634    )
 635
 636    axes_list = [ax1]
 637
 638    if plot_rainfall:
 639
 640        if len(columns) > 0:
 641            col = columns[0]
 642        else:
 643            col = 0
 644
 645        ax2.bar(
 646            xtics[:],
 647            model.atmos_data.mean_prcp[col, :],
 648            label="Average rainfall (mm)",
 649            width=np.timedelta64(int(date_range[2]), "s"),
 650            color="blue",
 651        )
 652
 653        ax2.invert_yaxis()
 654        ax2.grid(alpha=0.7, ls="--")
 655        ax2.get_xaxis().set_visible(False)
 656        ax2.set_ylim(
 657            bottom=1.2 * max(model.atmos_data.mean_prcp[0, :]), top=0.0
 658        )
 659        ax2.set_ylabel("Average rainfall (mm)")
 660
 661        axes_list.append(ax2)
 662
 663    fig, ax = fig_settings.change(figure=(fig, tuple(axes_list)))
 664
 665    return fig, ax
 666
 667
 668def plot_catchment_surface_error(
 669    mesh: dict = None,
 670    ax_settings: dict | ax_properties = {},
 671    fig_settings: dict | fig_properties = {},
 672):
 673    """
 674    Plot the misfit criteria between the simulated and observed discharges.
 675    Parameters:
 676    -----------
 677    values: np.ndarray
 678        The result of the discharge misfit for all outlets.
 679    names: np.ndarray
 680        Outlets name or code stored in an np.ndarray.
 681    columns: list | None
 682        Columns of the np.ndarray to plot
 683    misfit: str
 684        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
 685    figure: tuple
 686        input figure as (fig,ax) to add a new curve
 687    ax_settings: dict or class ax_properties
 688        object or dict with any attribute of class ax_properties
 689    fig_settings: dict or class ax_properties
 690        object or dict with any attribute of class fig_settings
 691    """
 692
 693    if isinstance(ax_settings, dict):
 694        default_ax_settings = ax_properties(
 695            title="Catchment surface error (Ssim-Sobs)/Sobs *100",
 696            ylabel="Surface error %",
 697            xlabel="Outlets",
 698            xticklabels_rotation=45,
 699            xtics_fontsize=6,
 700        )
 701        default_ax_settings.update(**ax_settings)
 702    else:
 703        default_ax_settings = ax_properties(**ax_settings.__dict__)
 704
 705    if isinstance(fig_settings, dict):
 706        fig_settings = fig_properties(**fig_settings)
 707    else:
 708        fig_settings = fig_properties(**fig_settings.__dict__)
 709
 710    if len(mesh["code"]) == 0:
 711        print("Cannot plot this, the mesh has no gauge !")
 712        return None, None
 713
 714    plt.rcParams.update(plt.rcParamsDefault)
 715    fig, ax = plt.subplots()
 716    fig, ax = default_ax_settings.change(figure=(fig, ax))
 717
 718    surface_error = (mesh["area_dln"] - mesh["area"]) / mesh["area"] * 100
 719
 720    fig, ax = default_ax_settings.change(figure=(fig, ax))
 721    bar_container = ax.bar(
 722        mesh["code"], surface_error, color="grey", tick_label=mesh["code"]
 723    )
 724
 725    ax.bar_label(
 726        bar_container,
 727        fmt=lambda x: f"{x:.2f}",
 728        fontsize=default_ax_settings.barlabel_fontsize,
 729    )
 730
 731    fig, ax = default_ax_settings.change(figure=(fig, ax))
 732    fig, ax = fig_settings.change(figure=(fig, ax))
 733
 734    return fig, ax
 735
 736
 737def plot_catchment_surface_consistency(
 738    mesh: dict = None,
 739    label: bool = True,
 740    ax_settings: dict | ax_properties = {},
 741    fig_settings: dict | fig_properties = {},
 742    plot_settings: dict | plot_properties = {},
 743):
 744    """
 745    Plot the modeled surface vs the observed surface
 746    Parameters:
 747    -----------
 748    mesh: dict, optional
 749        The mesh of the Smash model, defaults to None
 750    ax_settings: dict or class ax_properties
 751        object or dict with any attribute of class ax_properties
 752    fig_settings: dict or class ax_properties
 753        object or dict with any attribute of class fig_settings
 754    plot_settings: dict or class plot_properties
 755        object or dict with any attribute of class plot_settings.Control the simulated curve
 756    """
 757
 758    if isinstance(ax_settings, dict):
 759        default_ax_settings = ax_properties(
 760            title="Modeled and observed surface consistency",
 761            ylabel="Modeled surface",
 762            xlabel="Observed surface",
 763        )
 764        default_ax_settings.update(**ax_settings)
 765    else:
 766        default_ax_settings = ax_properties(**ax_settings.__dict__)
 767
 768    if isinstance(fig_settings, dict):
 769        fig_settings = fig_properties(**fig_settings)
 770    else:
 771        fig_settings = fig_properties(**fig_settings.__dict__)
 772
 773    if isinstance(plot_settings, dict):
 774        default_plot_settings = plot_properties(
 775            marker="+",
 776            markersize=12,
 777            color="blue",
 778        )
 779        default_plot_settings.update(**plot_settings)
 780    else:
 781        default_plot_settings = plot_properties(**plot_settings.__dict__)
 782
 783    if len(mesh["code"]) == 0:
 784        print("Cannot plot this, the mesh has no gauge !")
 785        return None, None
 786
 787    surface_model = mesh["area_dln"] / 1000.0**2.0
 788    surface_obs = mesh["area"] / 1000.0**2.0
 789
 790    plt.rcParams.update(plt.rcParamsDefault)
 791    fig, ax = plt.subplots()
 792    fig, ax = default_ax_settings.change(figure=(fig, ax))
 793
 794    ax.plot(
 795        surface_obs,
 796        surface_model,
 797        markersize=default_plot_settings.markersize,
 798        marker=default_plot_settings.marker,
 799        color=default_plot_settings.color,
 800        linestyle="None",
 801    )
 802    ax.plot(
 803        np.linspace(min(surface_obs), max(surface_obs), 10),
 804        np.linspace(min(surface_obs), max(surface_obs), 10),
 805        linewidth=2,
 806        color="grey",
 807    )
 808
 809    if label:
 810        ha = ("left", "right")
 811        for i, label in enumerate(mesh["code"]):
 812            ax.annotate(
 813                label,  # this is the text
 814                (
 815                    surface_obs[i],
 816                    surface_model[i],
 817                ),  # these are the coordinates to position the label
 818                textcoords="data",  # how to position the text
 819                xytext=(
 820                    surface_obs[i],
 821                    surface_model[i],
 822                ),  # distance from text to points (x,y)
 823                ha=ha[
 824                    i % 2
 825                ],  # horizontal alignment can be left, right or center
 826                color="red",
 827                fontsize=default_ax_settings.annotate_fontsize,
 828            )
 829
 830    ax.set(
 831        xlabel=default_ax_settings.xlabel, ylabel=default_ax_settings.ylabel
 832    )
 833
 834    fig, ax = fig_settings.change(figure=(fig, ax))
 835
 836    return fig, ax
 837
 838
 839def plot_mesh(
 840    mesh: dict = None,
 841    coef_hydro: float = 99.0,
 842    catchment_polygon: None | DataFrame = None,
 843    ax_settings: dict | ax_properties = {},
 844    fig_settings: dict | fig_properties = {},
 845):
 846    """
 847    Plot the mesh of a smash model
 848    Parameters:
 849    -----------
 850    mesh: a smash mesh as dictionary
 851        a smash model object
 852    coef_hydro: float
 853        the coefficient to colorize the hydrographic network accodring the cumulative
 854        surface. default is 99% so that 99% of the cell will be hidden.
 855    ax_settings: dict or class ax_properties
 856        object or dict with any attribute of class ax_properties
 857    fig_settings: dict or class ax_properties
 858        object or dict with any attribute of class fig_settings
 859    """
 860
 861    if mesh is not None:
 862        if isinstance(mesh, dict):
 863            pass
 864        else:
 865            raise ValueError("mesh must be a dict")
 866    else:
 867        raise ValueError(
 868            "model or mesh are mandatory and must be a dict or a smash Model object"
 869        )
 870
 871    if isinstance(ax_settings, dict):
 872        default_ax_settings = ax_properties(
 873            title="Mesh of the Smash model",
 874            xlabel="x_coords",
 875            ylabel="y_coords",
 876        )
 877        default_ax_settings.update(**ax_settings)
 878    else:
 879        default_ax_settings = ax_properties(**ax_settings.__dict__)
 880
 881    if isinstance(fig_settings, dict):
 882        fig_settings = fig_properties(**fig_settings)
 883    else:
 884        fig_settings = fig_properties(**fig_settings.__dict__)
 885
 886    # mesh["active_cell"]
 887    gauge = mesh["gauge_pos"]
 888    stations = mesh["code"]
 889    flow_acc = mesh["flwacc"]
 890    na = mesh["active_cell"] == 0
 891
 892    flow_accum_bv = np.where(na, 0.0, flow_acc.data / 1000000.0)
 893    surfmin = (1.0 - coef_hydro / 100.0) * np.nanmax(flow_accum_bv)
 894    mask_flow = flow_accum_bv < surfmin
 895    flow_plot = np.where(mask_flow, np.nan, flow_accum_bv)
 896    flow_plot = np.where(na, np.nan, flow_plot)
 897
 898    plt.rcParams.update(plt.rcParamsDefault)
 899    fig, ax = plt.subplots()
 900    fig, ax = default_ax_settings.change(figure=(fig, ax))
 901
 902    bbox = geo_toolbox.get_bbox_from_smash_mesh(mesh)
 903    extent = (bbox["left"], bbox["right"], bbox["bottom"], bbox["top"])
 904
 905    active_cell = np.where(na, np.nan, mesh["active_cell"])
 906    cmap = ListedColormap(["lightgray"])
 907    ax.imshow(active_cell, cmap=cmap, extent=extent)
 908
 909    myblues = matplotlib.colormaps["Blues"]
 910    cmp = ListedColormap(myblues(np.linspace(0.30, 1.0, 265)))
 911    im = ax.imshow(flow_plot, cmap=cmp, extent=extent)
 912
 913    if catchment_polygon is not None:
 914        # catchment_polygon = gpd.read_file(outlets_shapefile)
 915        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
 916
 917    # create an axes on the right side of ax. The width of cax will be 5%
 918    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
 919    divider = make_axes_locatable(ax)
 920    cax = divider.append_axes("right", size="5%", pad=0.05)
 921
 922    fig.colorbar(
 923        im,
 924        cmap="Blues",
 925        ax=ax,
 926        label="Cumulated surface (km²)",
 927        shrink=0.75,
 928        cax=cax,
 929    )
 930
 931    pos_y = -5
 932    ha = "right"
 933    for i in range(len(stations)):
 934        if pos_y > 0:
 935            pos_y = -10
 936        else:
 937            pos_y = 5
 938        # pos_y=-1*pos_y
 939
 940        if ha == "right":
 941            ha = "left"
 942            pos_x = 5
 943        else:
 944            ha = "right"
 945            # pos_x = -5
 946
 947        coord = geo_toolbox.rowcol_to_xy(
 948            gauge[i][0],
 949            gauge[i][1],
 950            mesh["xmin"],
 951            mesh["ymax"],
 952            mesh["xres"],
 953            mesh["yres"],
 954        ) + np.array(
 955            [
 956                mesh["dx"][gauge[i][0], gauge[i][1]] / 2,
 957                -mesh["dx"][gauge[i][0], gauge[i][1]] / 2,
 958            ]
 959        )
 960
 961        code = stations[i]
 962        ax.plot(coord[0], coord[1], color="green", marker="o", markersize=6)
 963        ax.annotate(
 964            code,  # this is the text
 965            # these are the coordinates to position the label
 966            (coord[0], coord[1]),
 967            # textcoords="offset points",  # how to position the text
 968            # xytext=(pos_x, pos_y),  # distance from text to points (x,y)
 969            textcoords="data",  # how to position the text
 970            xytext=(coord[0], coord[1]),  # distance from text to points (x,y)
 971            ha=ha,  # horizontal alignment can be left, right or center
 972            color="red",
 973            fontsize=10,
 974        )
 975
 976    fig, ax = default_ax_settings.change(figure=(fig, ax))
 977
 978    fig, ax = fig_settings.change(figure=(fig, ax))
 979
 980    return fig, ax
 981
 982
 983def plot_xy_quantile(
 984    res_quantile,
 985    X,
 986    Y,
 987    res_quantile_obs=None,
 988    gauge_pos=None,
 989    figure=None,
 990    ax_settings: dict | ax_properties = {},
 991    fig_settings: dict | fig_properties = {},
 992    plot_settings: dict | plot_properties = {},
 993):
 994    """
 995    Plot the discharges quantiles fitting at X,Y coordinates.
 996    Parameters:
 997    -----------
 998    res_quantile: dict
 999        The result of the discharge quantile computation.
1000    res_quantile_obs: dict
1001        The results of the observed discharges quantile. res_quantile_obs is a dict and must be computed by the function smashbox.stats.stats.quantile_obs()
1002    gauge_pos: int
1003        gauge_pos is the index of gauge in the Smash mesh for which the quantile_discharge are provided to the function.
1004    X: int
1005        Coordinates of the pixel in the row directions (X means row)
1006    Y: int
1007        Coordinates of the pixel in the column directions (Y means column)
1008    figure: tuple
1009        input figure as (fig,ax) to add a new curve
1010    ax_settings: dict or class ax_properties
1011        object or dict with any attribute of class ax_properties
1012    fig_settings: dict or class ax_properties
1013        object or dict with any attribute of class fig_settings
1014    """
1015    if isinstance(ax_settings, dict):
1016        default_ax_settings = ax_properties(
1017            xscale="log",
1018            xlabel=f"Return period (*{res_quantile['chunk_size']} days)",
1019            ylabel="Discharges (m³/s)",
1020            grid=True,
1021            legend=True,
1022        )
1023        default_ax_settings.update(**ax_settings)
1024    else:
1025        default_ax_settings = ax_properties(**ax_settings.__dict__)
1026
1027    if isinstance(fig_settings, dict):
1028        fig_settings = fig_properties(**fig_settings)
1029    else:
1030        fig_settings = fig_properties(**fig_settings.__dict__)
1031
1032    if isinstance(plot_settings, dict):
1033        default_plot_settings = plot_properties(markersize=10)
1034        default_plot_settings.update(**plot_settings)
1035    else:
1036        default_plot_settings = plot_properties(**plot_settings.__dict__)
1037
1038    quantile = res_quantile["Q_th"][X, Y]
1039    maxima = res_quantile["maxima"][X, Y]
1040    T_emp = res_quantile["T_emp"]
1041    loc = res_quantile["fit_loc"][X, Y]
1042    scale = res_quantile["fit_scale"][X, Y]
1043    shape = res_quantile["fit_shape"][X, Y]
1044    fit = res_quantile["fit"]
1045
1046    sorted_data = np.sort(maxima)
1047
1048    plt.rcParams.update(plt.rcParamsDefault)
1049    if figure is None:
1050        fig, ax = plt.subplots()
1051    else:
1052        fig, ax = figure
1053
1054    fig, ax = default_ax_settings.change(figure=(fig, ax))
1055
1056    if res_quantile_obs is not None and len(res_quantile_obs.keys()) > 0:
1057        if gauge_pos is None:
1058            raise ValueError(
1059                "gauge_pos is None. gauge_pos argument must be an integer corresponding to the gauge index."
1060            )
1061        maxima_obs = res_quantile_obs["maxima"][gauge_pos, :]
1062        T_emp_obs = res_quantile_obs["Temp"][gauge_pos, :]
1063
1064        ax.plot(
1065            T_emp_obs,
1066            maxima_obs,
1067            "o",
1068            label="Observed",
1069            color="black",
1070            markersize=default_plot_settings.markersize,
1071        )
1072
1073    ax.plot(
1074        T_emp,
1075        sorted_data,
1076        "o",
1077        label="Empirical",
1078        markersize=default_plot_settings.markersize,
1079    )
1080
1081    ax.plot(
1082        res_quantile["T"],
1083        quantile,
1084        "x",
1085        label="Theorical",
1086        markersize=default_plot_settings.markersize,
1087    )
1088
1089    Trange = np.linspace(1.1, np.nanmax(res_quantile["T"]), 50)
1090
1091    if fit == "gumbel":
1092        ax.plot(
1093            Trange,
1094            [stats.quantile_gumbel(T, loc, scale) for T in Trange],
1095            "r--",
1096            label=f"{fit} fitted",
1097            lw=default_plot_settings.lw,
1098        )
1099
1100    if fit == "gev":
1101        ax.plot(
1102            Trange,
1103            [stats.quantile_gev(T, shape, loc, scale) for T in Trange],
1104            "r--",
1105            label=f"{fit} fitted",
1106            lw=default_plot_settings.lw,
1107        )
1108
1109    if "Umax" in res_quantile.keys() and "Umin" in res_quantile.keys():
1110        if (
1111            res_quantile["Umax"] is not None
1112            and res_quantile["Umin"] is not None
1113        ):
1114            ax.plot(
1115                res_quantile["T"],
1116                res_quantile["Umax"][X, Y],
1117                "r--",
1118                label="Uncertainties (max)",
1119                color="grey",
1120                lw=default_plot_settings.lw,
1121            )
1122            ax.plot(
1123                res_quantile["T"],
1124                res_quantile["Umin"][X, Y],
1125                "r--",
1126                label="Uncertainties (min)",
1127                color="grey",
1128                lw=default_plot_settings.lw,
1129            )
1130
1131    fig, ax = default_ax_settings.change(figure=(fig, ax))
1132    fig, ax = fig_settings.change(figure=(fig, ax))
1133
1134    return fig, ax
1135
1136
1137def plot_image(
1138    matrice=np.zeros(shape=(2, 2)),
1139    bbox=None,
1140    vmin=None,
1141    vmax=None,
1142    mask=None,
1143    extend=None,
1144    catchment_polygon=None,
1145    figure=None,
1146    ax_settings: dict | ax_properties = {},
1147    fig_settings: dict | fig_properties = {},
1148):
1149    """
1150    Function for plotting a matrix as an image
1151
1152    Parameters
1153    ----------
1154    matrice : numpy array
1155        Matrix to be plotted
1156    bbox : list
1157        ["left","right","bottom","top"] bouding box to put x and y coordinates instead
1158    of the shape of the matrix
1159    vmin: real,
1160        minimum z value
1161    vmax: real,
1162        maximum z value
1163    mask: integer, matrix, shape of matice, contain 0 for pixels that should not be plotted
1164    catchment_polygon: dataframe containing some polygon to be plotted.
1165    Ideally it must contain the boundaries of the catchment as a polygon from a shp file
1166    read by geopanda.
1167    figure: tuple
1168        input figure as (fig,ax) to add a new curve
1169    ax_settings: dict or class ax_properties
1170        object or dict with any attribute of class ax_properties
1171    fig_settings: dict or class ax_properties
1172        object or dict with any attribute of class fig_settings
1173
1174    Examples
1175    ----------
1176    smash.utils.plot_image(mesh_france['drained_area'],bbox=bbox,title="Surfaces
1177                           drainƩes",xlabel="Longitude",ylabel="Latitude",zlabel="Surfaces drainƩes
1178                           km^2",vmin=0.0,vmax=1000,mask=mesh_france['global_active_cell'])
1179
1180    """
1181
1182    if isinstance(ax_settings, dict):
1183        ax_settings = ax_properties(**ax_settings)
1184    else:
1185        ax_settings = ax_properties(**ax_settings.__dict__)
1186
1187    if isinstance(fig_settings, dict):
1188        fig_settings = fig_properties(**fig_settings)
1189    else:
1190        fig_settings = fig_properties(**fig_settings.__dict__)
1191
1192    matrice = np.float32(matrice)
1193
1194    if bbox is not None:
1195        extent = [
1196            bbox["left"],
1197            bbox["right"],
1198            bbox["bottom"],
1199            bbox["top"],
1200        ]
1201    else:
1202        extent = None
1203
1204    if mask is not None:
1205        matrice[np.where(mask == 0)] = np.nan
1206
1207    plt.rcParams.update(plt.rcParamsDefault)
1208    if figure is None:
1209        fig, ax = plt.subplots()
1210    else:
1211        fig, ax = figure
1212
1213    if vmax is None:
1214        vmax = np.nanmax(matrice)
1215    if vmin is None:
1216        vmin = np.nanmin(matrice)
1217
1218    fig, ax = ax_settings.change(figure=(fig, ax))
1219
1220    # do it first otherwise crash if vmin>min de polygon
1221    if catchment_polygon is not None:
1222        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
1223
1224    im = ax.imshow(
1225        matrice, extent=extent, vmin=vmin, vmax=vmax, cmap=ax_settings.cmap
1226    )
1227
1228    # create an axes on the right side of ax. The width of cax will be 5%
1229    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1230    divider = make_axes_locatable(ax)
1231    cax = divider.append_axes("right", size="5%", pad=0.05)
1232
1233    plt.colorbar(im, label=ax_settings.clabel, cax=cax)
1234
1235    fig, ax = ax_settings.change(figure=(fig, ax))
1236    fig, ax = fig_settings.change(figure=(fig, ax))
1237
1238    return (fig, ax)
1239
1240
1241def plot_misfit(
1242    values: np.ndarray = [],
1243    names: np.ndarray = [],
1244    columns: list | None = None,
1245    misfit: str = "nse",
1246    figure: list | tuple | None = None,
1247    ax_settings: dict | ax_properties = {},
1248    fig_settings: dict | fig_properties = {},
1249):
1250    """
1251    Plot the misfit criteria between the simulated and observed discharges.
1252    Parameters:
1253    -----------
1254    values: np.ndarray
1255        The result of the discharge misfit for all outlets.
1256    names: np.ndarray
1257        Outlets name or code stored in an np.ndarray.
1258    columns: list | None
1259        Columns of the np.ndarray to plot
1260    misfit: str
1261        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
1262    figure: tuple
1263        input figure as (fig,ax) to add a new curve
1264    ax_settings: dict or class ax_properties
1265        object or dict with any attribute of class ax_properties
1266    fig_settings: dict or class ax_properties
1267        object or dict with any attribute of class fig_settings
1268    """
1269
1270    if isinstance(ax_settings, dict):
1271        default_ax_settings = ax_properties(
1272            ylabel=f"{misfit} criteria",
1273            xlabel="Gauges stations",
1274            grid=True,
1275            legend=True,
1276            xticklabels_rotation=45,
1277            xtics_fontsize=8,
1278        )
1279        default_ax_settings.update(**ax_settings)
1280    else:
1281        default_ax_settings = ax_properties(**ax_settings.__dict__)
1282
1283    if isinstance(fig_settings, dict):
1284        fig_settings = fig_properties(**fig_settings)
1285    else:
1286        fig_settings = fig_properties(**fig_settings.__dict__)
1287
1288    if len(names) == 0:
1289        names = np.arange(len(values))
1290
1291    if columns is not None:
1292        values = values[columns]
1293        names = names[columns]
1294
1295    # remove nan from plot
1296    columns = list(np.isnan(values) == False)
1297    # print(columns)
1298    if len(columns) > 0:
1299        values = values[columns]
1300        names = names[columns]
1301
1302    if figure is None:
1303        fig, ax = plt.subplots()
1304    else:
1305        fig, ax = figure
1306
1307    fig, ax = default_ax_settings.change(figure=(fig, ax))
1308    bar_container = ax.bar(names, values, color="grey", tick_label=names)
1309
1310    ax.bar_label(
1311        bar_container,
1312        fmt=lambda x: f"{x:.2f}",
1313        fontsize=default_ax_settings.barlabel_fontsize,
1314    )
1315
1316    fig, ax = default_ax_settings.change(figure=(fig, ax))
1317    fig, ax = fig_settings.change(figure=(fig, ax))
1318
1319    return fig, ax
1320
1321
1322def plot_outlet_stats(
1323    values_sim: np.ndarray | None = None,
1324    values_obs: np.ndarray | None = None,
1325    names: np.ndarray = [],
1326    columns: list | None = [],
1327    stat: str = "max",
1328    figure: list | tuple | None = None,
1329    ax_settings: dict | ax_properties = {},
1330    fig_settings: dict | fig_properties = {},
1331):
1332    """
1333    Plot a statistical criteria at a given list of outlet.
1334    Parameters:
1335    -----------
1336    values_sim: np.ndarray or None
1337        The result of the simulated stat for all outlets.
1338    values_obs: np.ndarray or None
1339        The result of the observed stat for all outlets.
1340    names: np.ndarray
1341        Outlets name or code stored in an np.ndarray.
1342    columns: list | None
1343        Columns of the np.ndarray to plot
1344    stat: str
1345        Criteria to plot. choice are ['max', 'min', 'mean', 'median', 'q20', 'q80']"
1346    figure: tuple
1347        input figure as (fig,ax) to add a new curve
1348    ax_settings: dict or class ax_properties
1349        object or dict with any attribute of class ax_properties
1350    fig_settings: dict or class ax_properties
1351        object or dict with any attribute of class fig_settings
1352    """
1353
1354    if isinstance(ax_settings, dict):
1355        default_ax_settings = ax_properties(
1356            ylabel=f"{stat} discharges (m3/s)",
1357            xlabel="Gauges stations",
1358            grid=True,
1359            legend=True,
1360            xticklabels_rotation=45,
1361            xtics_fontsize=6,
1362        )
1363        default_ax_settings.update(**ax_settings)
1364    else:
1365        default_ax_settings = ax_properties(**ax_settings.__dict__)
1366
1367    if isinstance(fig_settings, dict):
1368        fig_settings = fig_properties(**fig_settings)
1369    else:
1370        fig_settings = fig_properties(**fig_settings.__dict__)
1371
1372    if columns is not None:
1373        if values_sim is not None:
1374            values_sim = values_sim[columns]
1375
1376        if values_obs is not None:
1377            values_obs = values_obs[columns]
1378
1379        names = names[columns]
1380
1381    if np.all(values_obs == -99.0):
1382        values_obs = None
1383
1384    if values_sim is not None and values_obs is not None:
1385        if values_obs.size != values_sim.size:
1386            raise ValueError(
1387                "values_sim and values_obs must have the same size !"
1388            )
1389
1390    if figure is None:
1391        fig, ax = plt.subplots()
1392    else:
1393        fig, ax = figure
1394
1395    fig, ax = default_ax_settings.change(figure=(fig, ax))
1396
1397    x = np.arange(len(names))
1398    width = 0.25  # the width of the bars
1399
1400    multiplier = 0
1401
1402    if values_sim is not None:
1403        offset = width * multiplier
1404        ax.bar(x + offset, values_sim, width, label="obs")
1405        multiplier += 1
1406        # ax.bar_label(rects, padding=3)
1407
1408    if values_obs is not None:
1409        offset = width * multiplier
1410        ax.bar(x + offset, values_obs, width, label="sim")
1411        # ax.bar_label(rects, padding=3)
1412        # multiplier += 1
1413
1414    ax.set_xticks(x + width, names)
1415
1416    # bar_container = ax.bar(names, values, color="grey", tick_label=names)
1417
1418    # ax.bar_label(
1419    #     bar_container,
1420    #     fmt=lambda x: f"{x:.2f}",
1421    #     fontsize=default_ax_settings.barlabel_fontsize,
1422    # )
1423
1424    fig, ax = default_ax_settings.change(figure=(fig, ax))
1425    fig, ax = fig_settings.change(figure=(fig, ax))
1426
1427    return fig, ax
1428
1429
1430def plot_misfit_map(
1431    values: np.ndarray = [],
1432    names: np.ndarray = [],
1433    mesh=None,
1434    misfit: str = "nse",
1435    coef_hydro=99.0,
1436    catchment_polygon: None | DataFrame = None,
1437    ax_settings: dict | ax_properties = {},
1438    fig_settings: dict | fig_properties = {},
1439    plot_settings: dict | plot_properties = {},
1440):
1441    """
1442    Map plot of the misfit criteria between the simulated and observed discharges.
1443    Parameters:
1444    -----------
1445    values: np.ndarray
1446        The result of the discharge misfit for all outlets.
1447    names: np.ndarray
1448        Outlets name or code stored in an np.ndarray.
1449    mesh: None | dict
1450        The mesh of the Smash model as dict
1451    misfit: str
1452        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
1453    figure: tuple
1454        input figure as (fig,ax) to add a new curve
1455    ax_settings: dict or class ax_properties
1456        object or dict with any attribute of class ax_properties
1457    fig_settings: dict or class ax_properties
1458        object or dict with any attribute of class fig_settings
1459    plot_settings_sim: dict or class ax_properties
1460        object or dict with any attribute of class plot_settings.
1461    """
1462    if mesh is not None:
1463        if isinstance(mesh, dict):
1464            pass
1465        else:
1466            raise ValueError("mesh must be a dict")
1467    else:
1468        raise ValueError(
1469            "model or mesh are mandatory and must be a dict or a smash Model object"
1470        )
1471
1472    if isinstance(ax_settings, dict):
1473        default_ax_settings = ax_properties(
1474            title=f"Map of {misfit} criteria over the domain.",
1475            xlabel="x_coords",
1476            ylabel="y_coords",
1477            cmap="turbo_r",
1478        )
1479        default_ax_settings.update(**ax_settings)
1480    else:
1481        default_ax_settings = ax_properties(**ax_settings.__dict__)
1482
1483    if isinstance(fig_settings, dict):
1484        fig_settings = fig_properties(**fig_settings)
1485    else:
1486        fig_settings = fig_properties(**fig_settings.__dict__)
1487
1488    if isinstance(plot_settings, dict):
1489        default_plot_settings = plot_properties(
1490            marker="o",
1491            markersize=8,
1492        )
1493        default_plot_settings.update(**plot_settings)
1494    else:
1495        default_plot_settings = plot_properties(**plot_settings.__dict__)
1496
1497    # unset attribute color, managed separatly
1498    delattr(default_plot_settings, "color")
1499
1500    gauge = mesh["gauge_pos"]
1501    stations = mesh["code"]
1502    flow_acc = mesh["flwacc"]
1503    na = mesh["active_cell"] == 0
1504
1505    bbox = geo_toolbox.get_bbox_from_smash_mesh(mesh)
1506    extent = (bbox["left"], bbox["right"], bbox["bottom"], bbox["top"])
1507
1508    flow_accum_bv = np.where(na, 0.0, flow_acc.data)
1509    surfmin = (1.0 - coef_hydro / 100.0) * np.nanmax(flow_accum_bv)
1510    mask_flow = flow_accum_bv < surfmin
1511    flow_plot = np.where(mask_flow, np.nan, flow_accum_bv.data)
1512    flow_plot = np.where(na, np.nan, flow_plot)
1513
1514    plt.rcParams.update(plt.rcParamsDefault)
1515    fig, ax = plt.subplots()
1516    fig, ax = default_ax_settings.change(figure=(fig, ax))
1517
1518    active_cell = np.where(na, np.nan, mesh["active_cell"])
1519    cmap = ListedColormap(["lightgray"])
1520    ax.imshow(active_cell, cmap=cmap, extent=extent)
1521
1522    myblues = matplotlib.colormaps["binary"]
1523    cmp = ListedColormap(myblues(np.linspace(0.20, 1.0, 265)))
1524    im = ax.imshow(flow_plot, cmap=cmp, extent=extent)
1525
1526    if catchment_polygon is not None:
1527        # catchment_polygon = gpd.read_file(outlets_shapefile)
1528        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
1529
1530    # create an axes on the right side of ax. The width of cax will be 5%
1531    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1532    divider = make_axes_locatable(ax)
1533    cax = divider.append_axes("right", size="5%", pad=0.05)
1534
1535    fig.colorbar(
1536        im,
1537        cmap="Blues",
1538        ax=ax,
1539        label="Cumulated surface (km²)",
1540        shrink=0.75,
1541        cax=cax,
1542    )
1543
1544    # define bounds for the colormap
1545    if misfit == "nse" or misfit == "nnse":
1546        vmin = 0
1547        vmax = 1
1548    elif misfit == "rmse" or misfit == "nrmse" or misfit == "se":
1549        vmin = 0
1550        vmax = np.nanmax(values)
1551    else:
1552        vmin = np.nanmin(values)
1553        vmax = np.nanmax(values)
1554
1555    colormap = cm.get_cmap(default_ax_settings.cmap)
1556    cmp = ListedColormap(colormap(np.linspace(vmin, vmax, 256)))
1557
1558    ha = "right"
1559    for i in range(len(stations)):
1560
1561        if ha == "right":
1562            ha = "left"
1563            str_val = str(np.round(values[i], 2)).rjust(int(len(stations[i])))
1564            code = f"{stations[i]}\n {str_val}"
1565
1566        else:
1567            ha = "right"
1568            str_val = str(np.round(values[i], 2)).ljust(int(len(stations[i])))
1569            code = f"{stations[i]}\n {str_val}"
1570
1571        coord = geo_toolbox.rowcol_to_xy(
1572            gauge[i][0],
1573            gauge[i][1],
1574            mesh["xmin"],
1575            mesh["ymax"],
1576            mesh["xres"],
1577            mesh["yres"],
1578        )
1579
1580        ax.plot(
1581            coord[0],
1582            coord[1],
1583            color=cmp(values[i]),
1584            **default_plot_settings.__dict__,
1585        )
1586
1587        ax.annotate(
1588            code,  # this is the text
1589            # these are the coordinates to position the label
1590            (coord[0], coord[1]),
1591            textcoords="data",  # how to position the text
1592            xytext=(coord[0], coord[1]),  # distance from text to points (x,y)
1593            ha=ha,  # horizontal alignment can be left, right or center
1594            color=cmp(values[i]),
1595            fontsize=default_ax_settings.annotate_fontsize
1596            * default_ax_settings.font_ratio,
1597        )
1598
1599    import matplotlib as mpl
1600
1601    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
1602    # create an axes on the right side of ax. The width of cax will be 5%
1603    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1604    # divider = make_axes_locatable(ax)
1605    cax = divider.append_axes("right", size="5%", pad=0.5)
1606
1607    fig.colorbar(
1608        cm.ScalarMappable(norm=norm, cmap=cmp),
1609        cmap=cmp,
1610        ax=ax,
1611        cax=cax,
1612        label=misfit,
1613        shrink=0.75,
1614        location="right",
1615    )
1616
1617    fig, ax = default_ax_settings.change(figure=(fig, ax))
1618
1619    fig, ax = fig_settings.change(figure=(fig, ax))
1620
1621    return fig, ax
1622
1623
1624# def _ax_settings(
1625#     figure,
1626#     title: str = None,
1627#     xlabel: str = None,
1628#     ylabel: str = None,
1629#     clabel: str = None,
1630#     font_ratio: int = 1,
1631#     title_fontsize: int = 12,
1632#     label_fontsize: int = 10,
1633#     grid: bool = True,
1634#     xscale: str | None = None,
1635#     yscale: str | None = None,
1636#     legend: bool = True,
1637#     legend_loc: str = None,
1638#     legend_fontsize: int = 8,
1639#     xtics_fontsize: int = 8,
1640#     ytics_fontsize: int = 8,
1641#     cmap: str | None = None,
1642#     xlim: tuple | list | None = (None, None),
1643#     ylim: tuple | list | None = (None, None),
1644# ):
1645
1646#     fig, ax = figure
1647
1648#     plt.rcParams.update(plt.rcParamsDefault)
1649
1650#     if title is not None:
1651#         ax.set_title(title, fontsize=title_fontsize * font_ratio)
1652
1653#     if xlabel is not None:
1654#         ax.set_xlabel(xlabel)
1655
1656#     if ylabel is not None:
1657#         ax.set_ylabel(ylabel)
1658
1659#     if grid:
1660#         ax.grid(True, which="both", linestyle="--", alpha=0.5)
1661
1662#     if xscale is not None:
1663#         ax.set_xscale(xscale)
1664
1665#     if yscale is not None:
1666#         ax.set_xyscale(yscale)
1667
1668#     if legend:
1669#         ax.legend(loc=legend_loc)
1670
1671#     if cmap is not None:
1672#         plt.rc("image", cmap=cmap)
1673
1674#     if ylim[0] != None:
1675#         ax.set_ylim(bottom=ylim[0])
1676
1677#     if ylim[1] != None:
1678#         ax.set_ylim(top=ylim[1])
1679
1680#     if xlim[0] != None:
1681#         ax.set_xlim(left=xlim[0])
1682
1683#     if xlim[1] != None:
1684#         ax.set_xlim(right=xlim[1])
1685
1686#     plt.rcParams.update(
1687#         {
1688#             "axes.labelsize": label_fontsize * font_ratio,
1689#             "axes.titlesize": title_fontsize * font_ratio,
1690#             "legend.fontsize": legend_fontsize * font_ratio,
1691#             "figure.titlesize": title_fontsize * font_ratio,
1692#             "xtick.labelsize": xtics_fontsize * font_ratio,
1693#             "ytick.labelsize": ytics_fontsize * font_ratio,
1694#         }
1695#     )
1696
1697#     return fig, ax
1698
1699
1700# def _fig_settings(
1701#     figure,
1702#     figname=None,
1703#     xsize=8,
1704#     ysize=6,
1705#     transparent=False,
1706#     dpi=80,
1707#     font_ratio=1,
1708#     bbox_inches="tight",
1709# ):
1710
1711#     fig, ax = figure
1712
1713#     fig.set_figheight(ysize)
1714#     fig.set_figwidth(xsize)
1715
1716#     plt.rc(
1717#         "font", size=plt.rcParams["font.size"] * font_ratio
1718#     )  # controls default text sizes
1719#     plt.rc(
1720#         "axes", titlesize=plt.rcParams["axes.titlesize"] * font_ratio
1721#     )  # fontsize of the axes title
1722#     plt.rc(
1723#         "axes", labelsize=plt.rcParams["axes.labelsize"] * font_ratio
1724#     )  # fontsize of the x and y labels
1725#     plt.rc(
1726#         "xtick", labelsize=plt.rcParams["xtick.labelsize"] * font_ratio
1727#     )  # fontsize of the tick labels
1728#     plt.rc(
1729#         "ytick", labelsize=plt.rcParams["ytick.labelsize"] * font_ratio
1730#     )  # fontsize of the tick labels
1731#     plt.rc(
1732#         "legend", fontsize=plt.rcParams["legend.fontsize"] * font_ratio
1733#     )  # legend fontsize
1734#     plt.rc(
1735#         "figure", titlesize=plt.rcParams["figure.titlesize"] * font_ratio
1736#     )  # fontsize of the figure title
1737
1738#     if figname is not None:
1739#         fig.savefig(figname, transparent=transparent, dpi=dpi, bbox_inches=bbox_inches)
1740
1741#     return fig, ax
class plot_properties:
26class plot_properties:
27    """Class which handle differents properties of the matplotlib plot function.
28    All attributes can be defined by the user and the object plot_properties can be
29     passed to any smashbox plot function.
30    """
31
32    def __init__(
33        self,
34        ls="-",
35        lw=1.5,
36        marker="",
37        markersize=4,
38        color="black",
39        label="",
40    ):
41        self.ls = ls
42        """The style of the line (see matplotlib documentation)"""
43        self.lw = lw
44        """The linewidth of the line (see matplotlib documentation)"""
45        self.marker = marker
46        """The style of the marker (see matplotlib documentation)"""
47        self.markersize = markersize
48        """The size of the marker (see matplotlib documentation)"""
49        self.color = color
50        """The color of the line (see matplotlib documentation)"""
51        self.label = label
52        """The label of the line (see matplotlib documentation)"""
53
54    def update(self, **kwargs):
55        """Update the class attributes using kwarg (dictionnary)"""
56        for key, values in kwargs.items():
57            setattr(self, key, values)

Class which handle differents properties of the matplotlib plot function. All attributes can be defined by the user and the object plot_properties can be passed to any smashbox plot function.

plot_properties(ls='-', lw=1.5, marker='', markersize=4, color='black', label='')
32    def __init__(
33        self,
34        ls="-",
35        lw=1.5,
36        marker="",
37        markersize=4,
38        color="black",
39        label="",
40    ):
41        self.ls = ls
42        """The style of the line (see matplotlib documentation)"""
43        self.lw = lw
44        """The linewidth of the line (see matplotlib documentation)"""
45        self.marker = marker
46        """The style of the marker (see matplotlib documentation)"""
47        self.markersize = markersize
48        """The size of the marker (see matplotlib documentation)"""
49        self.color = color
50        """The color of the line (see matplotlib documentation)"""
51        self.label = label
52        """The label of the line (see matplotlib documentation)"""
ls

The style of the line (see matplotlib documentation)

lw

The linewidth of the line (see matplotlib documentation)

marker

The style of the marker (see matplotlib documentation)

markersize

The size of the marker (see matplotlib documentation)

color

The color of the line (see matplotlib documentation)

label

The label of the line (see matplotlib documentation)

def update(self, **kwargs):
54    def update(self, **kwargs):
55        """Update the class attributes using kwarg (dictionnary)"""
56        for key, values in kwargs.items():
57            setattr(self, key, values)

Update the class attributes using kwarg (dictionnary)

class ax_properties:
 60class ax_properties:
 61    """Class which handle differents properties of the matplotlib ax object
 62    (see matplotlib documentation). All attributes can be defined by the user and the
 63     object ax_properties can be passed to any smashbox plot function.
 64    """
 65
 66    def __init__(
 67        self,
 68        title: str = None,
 69        xlabel: str = None,
 70        ylabel: str = None,
 71        clabel: str = None,
 72        font_ratio: int = 1,
 73        title_fontsize: int = 12,
 74        label_fontsize: int = 10,
 75        annotate_fontsize: int = 6,
 76        grid: bool = True,
 77        xscale: str | None = None,
 78        yscale: str | None = None,
 79        legend: bool = True,
 80        legend_loc: str = None,
 81        legend_fontsize: int = 8,
 82        xtics_fontsize: int = 8,
 83        ytics_fontsize: int = 8,
 84        cmap: str | None = None,
 85        xlim: tuple | list | None = (None, None),
 86        ylim: tuple | list | None = (None, None),
 87        xticklabels_rotation: int = 0,
 88        barlabel_fontsize=6,
 89    ):
 90
 91        self.title = title
 92        """The title of the graphic"""
 93        self.xlabel = xlabel
 94        """The label of the x axis"""
 95        self.ylabel = ylabel
 96        """The label of the y axis"""
 97        self.clabel = clabel
 98        """The label of the colorbar"""
 99        self.font_ratio = font_ratio
100        """Ratio of the global fontsize"""
101        self.title_fontsize = title_fontsize
102        """The fontsize of the title"""
103        self.label_fontsize = label_fontsize
104        """The label fontsize"""
105        self.annotate_fontsize = annotate_fontsize
106        """The annotation fontsize in plot"""
107        self.grid = grid
108        """Set the grid (boolean), default True"""
109        self.xscale = xscale
110        """Scale of the x axis (see matplotlib documentation)"""
111        self.yscale = yscale
112        """Scale of the x axis (see matplotlib documentation)"""
113        self.legend = legend
114        """Set the legend, boolean, default is True"""
115        self.legend_loc = legend_loc
116        """Localisation of the legend"""
117        self.legend_fontsize = legend_fontsize
118        """The fontsize of the legend"""
119        self.xtics_fontsize = xtics_fontsize
120        """The fontsize of the xtics"""
121        self.ytics_fontsize = ytics_fontsize
122        """The fontsize of the ytics"""
123        self.cmap = cmap
124        """The name of the used colormap"""
125        self.xlim = xlim
126        """The limit of the x axis, tuple or list, default is (None,None)"""
127        self.ylim = ylim
128        """The limit of the y axis, tuple or list, default is (None,None)"""
129        self.xticklabels_rotation = xticklabels_rotation
130        """Angle of the xtics labels, float, default is 0."""
131        self.barlabel_fontsize = barlabel_fontsize * self.font_ratio
132        "Fontsize of the bar top label"
133
134    def update(self, **kwargs):
135        """Update the class attributes using kwarg (dictionnary)"""
136        for key, values in kwargs.items():
137            setattr(self, key, values)
138
139    def change(self, figure):
140        """Apply change to the current figure `figure`
141        Parameter:
142        ----------
143        figure : list or tuple
144            list of (fig, ax), figure and ax of a matplotlib subplot.
145        Return:
146        -------
147            a tuple of the modified (fig,ax)
148        """
149        fig, ax = figure
150
151        plt.rcParams.update(plt.rcParamsDefault)
152
153        plt.rcParams.update(
154            {
155                "axes.labelsize": self.label_fontsize * self.font_ratio,
156                "axes.titlesize": self.title_fontsize * self.font_ratio,
157                "legend.fontsize": self.legend_fontsize * self.font_ratio,
158                "figure.titlesize": self.title_fontsize * self.font_ratio,
159                "xtick.labelsize": self.xtics_fontsize * self.font_ratio,
160                "ytick.labelsize": self.ytics_fontsize * self.font_ratio,
161            }
162        )
163
164        if self.title is not None:
165
166            ax.set_title(
167                self.title, fontsize=self.title_fontsize * self.font_ratio
168            )
169
170        if self.xlabel is not None:
171            ax.set_xlabel(
172                self.xlabel, fontsize=self.label_fontsize * self.font_ratio
173            )
174
175        if self.ylabel is not None:
176            ax.set_ylabel(
177                self.ylabel, fontsize=self.label_fontsize * self.font_ratio
178            )
179
180        if self.grid:
181            ax.grid(True, which="both", linestyle="--", alpha=0.5)
182
183        if self.xscale is not None:
184            ax.set_xscale(self.xscale)
185
186        if self.yscale is not None:
187            ax.set_xyscale(self.yscale)
188
189        if self.legend:
190            ax.legend(
191                loc=self.legend_loc,
192                fontsize=self.legend_fontsize * self.font_ratio,
193            )
194
195        if self.cmap is not None:
196            plt.rc("image", cmap=self.cmap)
197
198        if self.ylim[0] is not None:
199            ax.set_ylim(bottom=self.ylim[0])
200
201        if self.ylim[1] is not None:
202            ax.set_ylim(top=self.ylim[1])
203
204        if self.xlim[0] is not None:
205            ax.set_xlim(left=self.xlim[0])
206
207        if self.xlim[1] is not None:
208            ax.set_xlim(right=self.xlim[1])
209
210        if self.xticklabels_rotation > 0:
211            ax.set_xticklabels(
212                ax.get_xticklabels(),
213                rotation=self.xticklabels_rotation,
214                ha="right",
215            )
216
217        return fig, ax

Class which handle differents properties of the matplotlib ax object (see matplotlib documentation). All attributes can be defined by the user and the object ax_properties can be passed to any smashbox plot function.

ax_properties( title: str = None, xlabel: str = None, ylabel: str = None, clabel: str = None, font_ratio: int = 1, title_fontsize: int = 12, label_fontsize: int = 10, annotate_fontsize: int = 6, grid: bool = True, xscale: str | None = None, yscale: str | None = None, legend: bool = True, legend_loc: str = None, legend_fontsize: int = 8, xtics_fontsize: int = 8, ytics_fontsize: int = 8, cmap: str | None = None, xlim: tuple | list | None = (None, None), ylim: tuple | list | None = (None, None), xticklabels_rotation: int = 0, barlabel_fontsize=6)
 66    def __init__(
 67        self,
 68        title: str = None,
 69        xlabel: str = None,
 70        ylabel: str = None,
 71        clabel: str = None,
 72        font_ratio: int = 1,
 73        title_fontsize: int = 12,
 74        label_fontsize: int = 10,
 75        annotate_fontsize: int = 6,
 76        grid: bool = True,
 77        xscale: str | None = None,
 78        yscale: str | None = None,
 79        legend: bool = True,
 80        legend_loc: str = None,
 81        legend_fontsize: int = 8,
 82        xtics_fontsize: int = 8,
 83        ytics_fontsize: int = 8,
 84        cmap: str | None = None,
 85        xlim: tuple | list | None = (None, None),
 86        ylim: tuple | list | None = (None, None),
 87        xticklabels_rotation: int = 0,
 88        barlabel_fontsize=6,
 89    ):
 90
 91        self.title = title
 92        """The title of the graphic"""
 93        self.xlabel = xlabel
 94        """The label of the x axis"""
 95        self.ylabel = ylabel
 96        """The label of the y axis"""
 97        self.clabel = clabel
 98        """The label of the colorbar"""
 99        self.font_ratio = font_ratio
100        """Ratio of the global fontsize"""
101        self.title_fontsize = title_fontsize
102        """The fontsize of the title"""
103        self.label_fontsize = label_fontsize
104        """The label fontsize"""
105        self.annotate_fontsize = annotate_fontsize
106        """The annotation fontsize in plot"""
107        self.grid = grid
108        """Set the grid (boolean), default True"""
109        self.xscale = xscale
110        """Scale of the x axis (see matplotlib documentation)"""
111        self.yscale = yscale
112        """Scale of the x axis (see matplotlib documentation)"""
113        self.legend = legend
114        """Set the legend, boolean, default is True"""
115        self.legend_loc = legend_loc
116        """Localisation of the legend"""
117        self.legend_fontsize = legend_fontsize
118        """The fontsize of the legend"""
119        self.xtics_fontsize = xtics_fontsize
120        """The fontsize of the xtics"""
121        self.ytics_fontsize = ytics_fontsize
122        """The fontsize of the ytics"""
123        self.cmap = cmap
124        """The name of the used colormap"""
125        self.xlim = xlim
126        """The limit of the x axis, tuple or list, default is (None,None)"""
127        self.ylim = ylim
128        """The limit of the y axis, tuple or list, default is (None,None)"""
129        self.xticklabels_rotation = xticklabels_rotation
130        """Angle of the xtics labels, float, default is 0."""
131        self.barlabel_fontsize = barlabel_fontsize * self.font_ratio
132        "Fontsize of the bar top label"
title

The title of the graphic

xlabel

The label of the x axis

ylabel

The label of the y axis

clabel

The label of the colorbar

font_ratio

Ratio of the global fontsize

title_fontsize

The fontsize of the title

label_fontsize

The label fontsize

annotate_fontsize

The annotation fontsize in plot

grid

Set the grid (boolean), default True

xscale

Scale of the x axis (see matplotlib documentation)

yscale

Scale of the x axis (see matplotlib documentation)

legend

Set the legend, boolean, default is True

legend_loc

Localisation of the legend

legend_fontsize

The fontsize of the legend

xtics_fontsize

The fontsize of the xtics

ytics_fontsize

The fontsize of the ytics

cmap

The name of the used colormap

xlim

The limit of the x axis, tuple or list, default is (None,None)

ylim

The limit of the y axis, tuple or list, default is (None,None)

xticklabels_rotation

Angle of the xtics labels, float, default is 0.

barlabel_fontsize

Fontsize of the bar top label

def update(self, **kwargs):
134    def update(self, **kwargs):
135        """Update the class attributes using kwarg (dictionnary)"""
136        for key, values in kwargs.items():
137            setattr(self, key, values)

Update the class attributes using kwarg (dictionnary)

def change(self, figure):
139    def change(self, figure):
140        """Apply change to the current figure `figure`
141        Parameter:
142        ----------
143        figure : list or tuple
144            list of (fig, ax), figure and ax of a matplotlib subplot.
145        Return:
146        -------
147            a tuple of the modified (fig,ax)
148        """
149        fig, ax = figure
150
151        plt.rcParams.update(plt.rcParamsDefault)
152
153        plt.rcParams.update(
154            {
155                "axes.labelsize": self.label_fontsize * self.font_ratio,
156                "axes.titlesize": self.title_fontsize * self.font_ratio,
157                "legend.fontsize": self.legend_fontsize * self.font_ratio,
158                "figure.titlesize": self.title_fontsize * self.font_ratio,
159                "xtick.labelsize": self.xtics_fontsize * self.font_ratio,
160                "ytick.labelsize": self.ytics_fontsize * self.font_ratio,
161            }
162        )
163
164        if self.title is not None:
165
166            ax.set_title(
167                self.title, fontsize=self.title_fontsize * self.font_ratio
168            )
169
170        if self.xlabel is not None:
171            ax.set_xlabel(
172                self.xlabel, fontsize=self.label_fontsize * self.font_ratio
173            )
174
175        if self.ylabel is not None:
176            ax.set_ylabel(
177                self.ylabel, fontsize=self.label_fontsize * self.font_ratio
178            )
179
180        if self.grid:
181            ax.grid(True, which="both", linestyle="--", alpha=0.5)
182
183        if self.xscale is not None:
184            ax.set_xscale(self.xscale)
185
186        if self.yscale is not None:
187            ax.set_xyscale(self.yscale)
188
189        if self.legend:
190            ax.legend(
191                loc=self.legend_loc,
192                fontsize=self.legend_fontsize * self.font_ratio,
193            )
194
195        if self.cmap is not None:
196            plt.rc("image", cmap=self.cmap)
197
198        if self.ylim[0] is not None:
199            ax.set_ylim(bottom=self.ylim[0])
200
201        if self.ylim[1] is not None:
202            ax.set_ylim(top=self.ylim[1])
203
204        if self.xlim[0] is not None:
205            ax.set_xlim(left=self.xlim[0])
206
207        if self.xlim[1] is not None:
208            ax.set_xlim(right=self.xlim[1])
209
210        if self.xticklabels_rotation > 0:
211            ax.set_xticklabels(
212                ax.get_xticklabels(),
213                rotation=self.xticklabels_rotation,
214                ha="right",
215            )
216
217        return fig, ax

Apply change to the current figure figure

Parameter:

figure : list or tuple list of (fig, ax), figure and ax of a matplotlib subplot.

Return:

a tuple of the modified (fig,ax)
class fig_properties:
220class fig_properties:
221    """Class which handle differents properties of the matplotlib fig object
222    (see matplotlib documentation). All attributes can be defined by the user and the
223     object ax_properties can be passed to any smashbox plot function.
224    """
225
226    def __init__(
227        self,
228        figname=None,
229        xsize=8,
230        ysize=6,
231        transparent=False,
232        dpi=160,
233        font_ratio=1,
234        bbox_inches="tight",
235    ):
236
237        self.figname = figname
238        """Path to the figure name to be saved"""
239        self.xsize = xsize
240        """Width of the figure in inch"""
241        self.ysize = ysize
242        """Height of the figure in inch"""
243        self.transparent = transparent
244        """Use transparency when exporting the figure, default is False"""
245        self.dpi = dpi
246        """RƩsolution (dpi), int, default is 80"""
247        self.font_ratio = font_ratio
248        """Global font ratio"""
249        self.bbox_inches = bbox_inches
250        """Constraint of the boundingbox of each ax (see matplotlib docuentation)"""
251
252    def update(self, **kwargs):
253        """Update the class attributes using kwarg (dictionnary)"""
254
255        for key, values in kwargs.items():
256            setattr(self, key, values)
257
258    def change(self, figure):
259        """Apply change to the current figure `figure`
260        Parameter:
261        ----------
262        figure : list or tuple
263            list of (fig, ax), figure and ax of a matplotlib subplot.
264        Return:
265        -------
266            a tuple of the modified (fig,ax)
267        """
268
269        fig, ax = figure
270
271        fig.set_figheight(self.ysize)
272        fig.set_figwidth(self.xsize)
273
274        plt.rc(
275            "font", size=plt.rcParams["font.size"] * self.font_ratio
276        )  # controls default text sizes
277        plt.rc(
278            "axes", titlesize=plt.rcParams["axes.titlesize"] * self.font_ratio
279        )  # fontsize of the axes title
280        plt.rc(
281            "axes", labelsize=plt.rcParams["axes.labelsize"] * self.font_ratio
282        )  # fontsize of the x and y labels
283        plt.rc(
284            "xtick",
285            labelsize=plt.rcParams["xtick.labelsize"] * self.font_ratio,
286        )  # fontsize of the tick labels
287        plt.rc(
288            "ytick",
289            labelsize=plt.rcParams["ytick.labelsize"] * self.font_ratio,
290        )  # fontsize of the tick labels
291        plt.rc(
292            "legend",
293            fontsize=plt.rcParams["legend.fontsize"] * self.font_ratio,
294        )  # legend fontsize
295        plt.rc(
296            "figure",
297            titlesize=plt.rcParams["figure.titlesize"] * self.font_ratio,
298        )  # fontsize of the figure title
299
300        if self.figname is not None:
301
302            head_path, basename = os.path.split(self.figname)
303
304            if len(head_path) > 0 and not os.path.exists(head_path):
305                os.makedirs(head_path)
306
307            fig.savefig(
308                self.figname,
309                transparent=self.transparent,
310                dpi=self.dpi,
311                bbox_inches=self.bbox_inches,
312            )
313
314        return fig, ax

Class which handle differents properties of the matplotlib fig object (see matplotlib documentation). All attributes can be defined by the user and the object ax_properties can be passed to any smashbox plot function.

fig_properties( figname=None, xsize=8, ysize=6, transparent=False, dpi=160, font_ratio=1, bbox_inches='tight')
226    def __init__(
227        self,
228        figname=None,
229        xsize=8,
230        ysize=6,
231        transparent=False,
232        dpi=160,
233        font_ratio=1,
234        bbox_inches="tight",
235    ):
236
237        self.figname = figname
238        """Path to the figure name to be saved"""
239        self.xsize = xsize
240        """Width of the figure in inch"""
241        self.ysize = ysize
242        """Height of the figure in inch"""
243        self.transparent = transparent
244        """Use transparency when exporting the figure, default is False"""
245        self.dpi = dpi
246        """RƩsolution (dpi), int, default is 80"""
247        self.font_ratio = font_ratio
248        """Global font ratio"""
249        self.bbox_inches = bbox_inches
250        """Constraint of the boundingbox of each ax (see matplotlib docuentation)"""
figname

Path to the figure name to be saved

xsize

Width of the figure in inch

ysize

Height of the figure in inch

transparent

Use transparency when exporting the figure, default is False

dpi

RƩsolution (dpi), int, default is 80

font_ratio

Global font ratio

bbox_inches

Constraint of the boundingbox of each ax (see matplotlib docuentation)

def update(self, **kwargs):
252    def update(self, **kwargs):
253        """Update the class attributes using kwarg (dictionnary)"""
254
255        for key, values in kwargs.items():
256            setattr(self, key, values)

Update the class attributes using kwarg (dictionnary)

def change(self, figure):
258    def change(self, figure):
259        """Apply change to the current figure `figure`
260        Parameter:
261        ----------
262        figure : list or tuple
263            list of (fig, ax), figure and ax of a matplotlib subplot.
264        Return:
265        -------
266            a tuple of the modified (fig,ax)
267        """
268
269        fig, ax = figure
270
271        fig.set_figheight(self.ysize)
272        fig.set_figwidth(self.xsize)
273
274        plt.rc(
275            "font", size=plt.rcParams["font.size"] * self.font_ratio
276        )  # controls default text sizes
277        plt.rc(
278            "axes", titlesize=plt.rcParams["axes.titlesize"] * self.font_ratio
279        )  # fontsize of the axes title
280        plt.rc(
281            "axes", labelsize=plt.rcParams["axes.labelsize"] * self.font_ratio
282        )  # fontsize of the x and y labels
283        plt.rc(
284            "xtick",
285            labelsize=plt.rcParams["xtick.labelsize"] * self.font_ratio,
286        )  # fontsize of the tick labels
287        plt.rc(
288            "ytick",
289            labelsize=plt.rcParams["ytick.labelsize"] * self.font_ratio,
290        )  # fontsize of the tick labels
291        plt.rc(
292            "legend",
293            fontsize=plt.rcParams["legend.fontsize"] * self.font_ratio,
294        )  # legend fontsize
295        plt.rc(
296            "figure",
297            titlesize=plt.rcParams["figure.titlesize"] * self.font_ratio,
298        )  # fontsize of the figure title
299
300        if self.figname is not None:
301
302            head_path, basename = os.path.split(self.figname)
303
304            if len(head_path) > 0 and not os.path.exists(head_path):
305                os.makedirs(head_path)
306
307            fig.savefig(
308                self.figname,
309                transparent=self.transparent,
310                dpi=self.dpi,
311                bbox_inches=self.bbox_inches,
312            )
313
314        return fig, ax

Apply change to the current figure figure

Parameter:

figure : list or tuple list of (fig, ax), figure and ax of a matplotlib subplot.

Return:

a tuple of the modified (fig,ax)
def save_figure(*args, **kwargs):
 95    def wrapper(*args, **kwargs):
 96
 97        bound = sig.bind(*args, **kwargs)
 98        bound.apply_defaults()
 99
100        for name, value in bound.arguments.items():
101            if name in annotations:
102
103                target_type = annotations[name]
104
105                args_ = get_args(target_type)
106
107                if target_type is None and len(args_) == 0:
108                    args_ = (type(None),)
109                    target_type = type(None)
110
111                if not type(value) in args_:
112
113                    if len(args_) > 1 and type(None) in args_:
114
115                        converted = False
116                        for t in args_:
117
118                            if t is not type(None):
119
120                                if value is not None:
121                                    try:
122                                        print(
123                                            f"</> Warning: Arg '{name}' of type {type(value)} is being"
124                                            f" converted to {t}"
125                                        )
126                                        bound.arguments[name] = t(value)
127                                        converted = True
128                                    except:
129                                        pass
130
131                                if converted:
132                                    break
133
134                        if not converted:
135                            raise TypeError(
136                                f"</> Error: Arg '{name}' must be a type of "
137                                f" {args_}, got {value}"
138                                f" ({type(value).__name__})"
139                            )
140
141                    else:
142                        if not isinstance(value, target_type):
143                            try:
144                                print(
145                                    f"</> Warning: Arg '{name}' of type {type(value)} is being"
146                                    f" converted to {target_type}"
147                                )
148                                bound.arguments[name] = target_type(value)
149                            except Exception:
150                                raise TypeError(
151                                    f"</> Error: Arg '{name}' must be a type of "
152                                    f" {target_type.__name__}, got {value}"
153                                    f" ({type(value).__name__})"
154                                )
155
156        return func(*bound.args, **bound.kwargs)

Save a figure.

Parameters:

fig: fig object returned by matplotlib.subplot the figure to save figname: str Path to the figure xsize: int width of the figure in inch ysize: int height of the figure in inch transparent: bool, default is False use transparency dpi : int resolution of the figure, default is 80

def generate_palette(base_color, n, variation='hue'):
342def generate_palette(base_color, n, variation="hue"):
343    """
344    Generate a palette of colors from a base color.
345    Parameter:
346    ---------
347    base_color: str
348        matplotlib color string
349    n: int
350        number of color to generate
351    variation: 'hue' | 'brightness'
352        how to generate the color palette, by changing the hue or the brighness of the base color.
353    Return: a list of colors
354    """
355    # Convertir la couleur de base en format RGB normalisƩ (0-1)
356    rgb = mcolors.to_rgb(base_color)
357
358    # Convertir en HSV
359    h, s, v = colorsys.rgb_to_hsv(*rgb)
360
361    # GƩnƩrer n couleurs en modifiant la teinte ou valeur
362    palette = []
363    for i in range(n):
364        new_h = h
365        if variation == "hue":
366            new_h = (h + i / n) % 1.0  # cycle dans le cercle chromatique
367        new_v = v
368        if variation == "brightness":
369            new_v = max(
370                0.1, min(1.0, v * (0.5 + i / (2 * n)))
371            )  # Ʃviter le noir complet
372
373        new_rgb = colorsys.hsv_to_rgb(new_h, s, new_v)
374        palette.append(new_rgb)
375
376    return palette

Generate a palette of colors from a base color.

Parameter:

base_color: str matplotlib color string n: int number of color to generate variation: 'hue' | 'brightness' how to generate the color palette, by changing the hue or the brighness of the base color. Return: a list of colors

def plot_chro(*args, **kwargs):
 95    def wrapper(*args, **kwargs):
 96
 97        bound = sig.bind(*args, **kwargs)
 98        bound.apply_defaults()
 99
100        for name, value in bound.arguments.items():
101            if name in annotations:
102
103                target_type = annotations[name]
104
105                args_ = get_args(target_type)
106
107                if target_type is None and len(args_) == 0:
108                    args_ = (type(None),)
109                    target_type = type(None)
110
111                if not type(value) in args_:
112
113                    if len(args_) > 1 and type(None) in args_:
114
115                        converted = False
116                        for t in args_:
117
118                            if t is not type(None):
119
120                                if value is not None:
121                                    try:
122                                        print(
123                                            f"</> Warning: Arg '{name}' of type {type(value)} is being"
124                                            f" converted to {t}"
125                                        )
126                                        bound.arguments[name] = t(value)
127                                        converted = True
128                                    except:
129                                        pass
130
131                                if converted:
132                                    break
133
134                        if not converted:
135                            raise TypeError(
136                                f"</> Error: Arg '{name}' must be a type of "
137                                f" {args_}, got {value}"
138                                f" ({type(value).__name__})"
139                            )
140
141                    else:
142                        if not isinstance(value, target_type):
143                            try:
144                                print(
145                                    f"</> Warning: Arg '{name}' of type {type(value)} is being"
146                                    f" converted to {target_type}"
147                                )
148                                bound.arguments[name] = target_type(value)
149                            except Exception:
150                                raise TypeError(
151                                    f"</> Error: Arg '{name}' must be a type of "
152                                    f" {target_type.__name__}, got {value}"
153                                    f" ({type(value).__name__})"
154                                )
155
156        return func(*bound.args, **bound.kwargs)

Plot a temporal chonic of values

Parameters:

data: np.ndarray of dimenion 2. data to plot as a matrix of 2 dimension. t_axis : int the axis of the time in data, default is 1 outlets_name: list the list of the outlets name columns : list the column to be plotted in t_axis direction dt: float the timestep xtics : list list of date for the xtics. The format must be automatically read by numpy.Datetime date_range: list list of [date_start, date_end, timedelta] to generate the xtics figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings plot_settings: dict or class plot_properties object or dict with any attribute of class plot_settings

def plot_hydrograph( model: smash.core.model.model.Model | None = None, columns: list | tuple = [], outlets_name: list | tuple = [], plot_rainfall: bool = True, plot_qobs: bool = True, figure: list | tuple | None = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}, plot_settings_sim: dict | plot_properties = {}, plot_settings_obs: dict | plot_properties = {}):
493def plot_hydrograph(
494    model: Model | None = None,
495    columns: list | tuple = [],
496    outlets_name: list | tuple = [],
497    plot_rainfall: bool = True,
498    plot_qobs: bool = True,
499    figure: list | tuple | None = None,
500    ax_settings: dict | ax_properties = {},
501    fig_settings: dict | fig_properties = {},
502    plot_settings_sim: dict | plot_properties = {},
503    plot_settings_obs: dict | plot_properties = {},
504):
505    """
506    Plot an hydrograph from a smash model
507    Parameters:
508    -----------
509    model: a smash model object
510        a smash model object
511    outlets_name: list
512        the list of the outlets name
513    columns : list
514        the column to be plotted in t_axis direction
515    figure: tuple
516        input figure as (fig,ax) to add a new curve
517    ax_settings: dict or class ax_properties
518        object or dict with any attribute of class ax_properties
519    fig_settings: dict or class ax_properties
520        object or dict with any attribute of class fig_settings
521    plot_settings_sim: dict or class ax_properties
522        object or dict with any attribute of class plot_settings.Control the simulated curve
523    plot_settings_obs: dict or class ax_properties
524        object or dict with any attribute of class plot_settings. Control the observed curve
525
526    """
527    if model is None:
528        raise ValueError("Input smash model object is None.")
529
530    if isinstance(ax_settings, dict):
531        default_ax_settings = ax_properties(
532            xlabel="Time",
533            ylabel="discharges m^3/s",
534            xtics_fontsize=10,
535            ytics_fontsize=10,
536        )
537        default_ax_settings.update(**ax_settings)
538    else:
539        default_ax_settings = ax_properties(**ax_settings.__dict__)
540
541    if isinstance(fig_settings, dict):
542        fig_settings = fig_properties(**fig_settings)
543    else:
544        fig_settings = fig_properties(**fig_settings.__dict__)
545
546    if isinstance(plot_settings_sim, dict):
547        default_plot_settings_sim = plot_properties(
548            ls="-",
549            lw="2",
550            marker="",
551            markersize=4,
552            color="blue",
553            label="Sim",
554        )
555        default_plot_settings_sim.update(**plot_settings_sim)
556    else:
557        default_plot_settings_sim = plot_properties(
558            **plot_settings_sim.__dict__
559        )
560
561    # default color for multi curves: same color but different line type
562    if len(columns) >= 2:
563        color = "blue"
564    else:
565        color = "black"
566
567    if isinstance(plot_settings_obs, dict):
568        default_plot_settings_obs = plot_properties(
569            ls="--",
570            lw="1.5",
571            marker="",
572            markersize=4,
573            color=color,
574            label="Obs",
575        )
576        default_plot_settings_obs.update(**plot_settings_obs)
577    else:
578        default_plot_settings_obs = plot_properties()
579
580    # manage date here
581    date_deb = datetime.datetime.fromisoformat(
582        model.setup.start_time
583    ) + datetime.timedelta(seconds=int(model.setup.dt))
584    date_end = datetime.datetime.fromisoformat(model.setup.end_time)
585    date_range = [date_deb, date_end, model.setup.dt]
586
587    if figure is None:
588        if plot_rainfall:
589            fig, (ax2, ax1) = plt.subplots(2, 1, height_ratios=[1, 4])
590            fig.subplots_adjust(hspace=0)
591            figure = [fig, ax1, ax2]
592        else:
593            fig, ax2 = plt.subplots()
594            figure = [fig, ax1]
595    else:
596        if plot_rainfall:
597            fig = figure[0]
598            ax1 = figure[1]
599            ax2 = figure[2]
600        else:
601            fig = figure[0]
602            ax1 = figure[1]
603
604    fig, ax = default_ax_settings.change(figure=(fig, ax1))
605
606    if plot_qobs:
607        fig, ax1 = plot_chro(
608            np.where(model.response_data.q < 0, np.nan, model.response_data.q),
609            date_range=date_range,
610            columns=columns,
611            outlets_name=["obs_" + name for name in outlets_name],
612            figure=(fig, ax1),
613            ax_settings=default_ax_settings,
614            fig_settings=fig_settings,
615            plot_settings=default_plot_settings_obs,
616        )
617
618    fig, ax1 = plot_chro(
619        model.response.q,
620        date_range=date_range,
621        columns=columns,
622        outlets_name=["sim_" + name for name in outlets_name],
623        figure=(fig, ax1),
624        ax_settings=default_ax_settings,
625        fig_settings=fig_settings,
626        plot_settings=default_plot_settings_sim,
627    )
628
629    xtics = np.arange(
630        np.datetime64(date_range[0]),
631        np.datetime64(
632            date_range[1] + datetime.timedelta(seconds=int(date_range[2]))
633        ),
634        np.timedelta64(int(date_range[2]), "s"),
635    )
636
637    axes_list = [ax1]
638
639    if plot_rainfall:
640
641        if len(columns) > 0:
642            col = columns[0]
643        else:
644            col = 0
645
646        ax2.bar(
647            xtics[:],
648            model.atmos_data.mean_prcp[col, :],
649            label="Average rainfall (mm)",
650            width=np.timedelta64(int(date_range[2]), "s"),
651            color="blue",
652        )
653
654        ax2.invert_yaxis()
655        ax2.grid(alpha=0.7, ls="--")
656        ax2.get_xaxis().set_visible(False)
657        ax2.set_ylim(
658            bottom=1.2 * max(model.atmos_data.mean_prcp[0, :]), top=0.0
659        )
660        ax2.set_ylabel("Average rainfall (mm)")
661
662        axes_list.append(ax2)
663
664    fig, ax = fig_settings.change(figure=(fig, tuple(axes_list)))
665
666    return fig, ax

Plot an hydrograph from a smash model

Parameters:

model: a smash model object a smash model object outlets_name: list the list of the outlets name columns : list the column to be plotted in t_axis direction figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings plot_settings_sim: dict or class ax_properties object or dict with any attribute of class plot_settings.Control the simulated curve plot_settings_obs: dict or class ax_properties object or dict with any attribute of class plot_settings. Control the observed curve

def plot_catchment_surface_error( mesh: dict = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}):
669def plot_catchment_surface_error(
670    mesh: dict = None,
671    ax_settings: dict | ax_properties = {},
672    fig_settings: dict | fig_properties = {},
673):
674    """
675    Plot the misfit criteria between the simulated and observed discharges.
676    Parameters:
677    -----------
678    values: np.ndarray
679        The result of the discharge misfit for all outlets.
680    names: np.ndarray
681        Outlets name or code stored in an np.ndarray.
682    columns: list | None
683        Columns of the np.ndarray to plot
684    misfit: str
685        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
686    figure: tuple
687        input figure as (fig,ax) to add a new curve
688    ax_settings: dict or class ax_properties
689        object or dict with any attribute of class ax_properties
690    fig_settings: dict or class ax_properties
691        object or dict with any attribute of class fig_settings
692    """
693
694    if isinstance(ax_settings, dict):
695        default_ax_settings = ax_properties(
696            title="Catchment surface error (Ssim-Sobs)/Sobs *100",
697            ylabel="Surface error %",
698            xlabel="Outlets",
699            xticklabels_rotation=45,
700            xtics_fontsize=6,
701        )
702        default_ax_settings.update(**ax_settings)
703    else:
704        default_ax_settings = ax_properties(**ax_settings.__dict__)
705
706    if isinstance(fig_settings, dict):
707        fig_settings = fig_properties(**fig_settings)
708    else:
709        fig_settings = fig_properties(**fig_settings.__dict__)
710
711    if len(mesh["code"]) == 0:
712        print("Cannot plot this, the mesh has no gauge !")
713        return None, None
714
715    plt.rcParams.update(plt.rcParamsDefault)
716    fig, ax = plt.subplots()
717    fig, ax = default_ax_settings.change(figure=(fig, ax))
718
719    surface_error = (mesh["area_dln"] - mesh["area"]) / mesh["area"] * 100
720
721    fig, ax = default_ax_settings.change(figure=(fig, ax))
722    bar_container = ax.bar(
723        mesh["code"], surface_error, color="grey", tick_label=mesh["code"]
724    )
725
726    ax.bar_label(
727        bar_container,
728        fmt=lambda x: f"{x:.2f}",
729        fontsize=default_ax_settings.barlabel_fontsize,
730    )
731
732    fig, ax = default_ax_settings.change(figure=(fig, ax))
733    fig, ax = fig_settings.change(figure=(fig, ax))
734
735    return fig, ax

Plot the misfit criteria between the simulated and observed discharges.

Parameters:

values: np.ndarray The result of the discharge misfit for all outlets. names: np.ndarray Outlets name or code stored in an np.ndarray. columns: list | None Columns of the np.ndarray to plot misfit: str Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']" figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

def plot_catchment_surface_consistency( mesh: dict = None, label: bool = True, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}, plot_settings: dict | plot_properties = {}):
738def plot_catchment_surface_consistency(
739    mesh: dict = None,
740    label: bool = True,
741    ax_settings: dict | ax_properties = {},
742    fig_settings: dict | fig_properties = {},
743    plot_settings: dict | plot_properties = {},
744):
745    """
746    Plot the modeled surface vs the observed surface
747    Parameters:
748    -----------
749    mesh: dict, optional
750        The mesh of the Smash model, defaults to None
751    ax_settings: dict or class ax_properties
752        object or dict with any attribute of class ax_properties
753    fig_settings: dict or class ax_properties
754        object or dict with any attribute of class fig_settings
755    plot_settings: dict or class plot_properties
756        object or dict with any attribute of class plot_settings.Control the simulated curve
757    """
758
759    if isinstance(ax_settings, dict):
760        default_ax_settings = ax_properties(
761            title="Modeled and observed surface consistency",
762            ylabel="Modeled surface",
763            xlabel="Observed surface",
764        )
765        default_ax_settings.update(**ax_settings)
766    else:
767        default_ax_settings = ax_properties(**ax_settings.__dict__)
768
769    if isinstance(fig_settings, dict):
770        fig_settings = fig_properties(**fig_settings)
771    else:
772        fig_settings = fig_properties(**fig_settings.__dict__)
773
774    if isinstance(plot_settings, dict):
775        default_plot_settings = plot_properties(
776            marker="+",
777            markersize=12,
778            color="blue",
779        )
780        default_plot_settings.update(**plot_settings)
781    else:
782        default_plot_settings = plot_properties(**plot_settings.__dict__)
783
784    if len(mesh["code"]) == 0:
785        print("Cannot plot this, the mesh has no gauge !")
786        return None, None
787
788    surface_model = mesh["area_dln"] / 1000.0**2.0
789    surface_obs = mesh["area"] / 1000.0**2.0
790
791    plt.rcParams.update(plt.rcParamsDefault)
792    fig, ax = plt.subplots()
793    fig, ax = default_ax_settings.change(figure=(fig, ax))
794
795    ax.plot(
796        surface_obs,
797        surface_model,
798        markersize=default_plot_settings.markersize,
799        marker=default_plot_settings.marker,
800        color=default_plot_settings.color,
801        linestyle="None",
802    )
803    ax.plot(
804        np.linspace(min(surface_obs), max(surface_obs), 10),
805        np.linspace(min(surface_obs), max(surface_obs), 10),
806        linewidth=2,
807        color="grey",
808    )
809
810    if label:
811        ha = ("left", "right")
812        for i, label in enumerate(mesh["code"]):
813            ax.annotate(
814                label,  # this is the text
815                (
816                    surface_obs[i],
817                    surface_model[i],
818                ),  # these are the coordinates to position the label
819                textcoords="data",  # how to position the text
820                xytext=(
821                    surface_obs[i],
822                    surface_model[i],
823                ),  # distance from text to points (x,y)
824                ha=ha[
825                    i % 2
826                ],  # horizontal alignment can be left, right or center
827                color="red",
828                fontsize=default_ax_settings.annotate_fontsize,
829            )
830
831    ax.set(
832        xlabel=default_ax_settings.xlabel, ylabel=default_ax_settings.ylabel
833    )
834
835    fig, ax = fig_settings.change(figure=(fig, ax))
836
837    return fig, ax

Plot the modeled surface vs the observed surface

Parameters:

mesh: dict, optional The mesh of the Smash model, defaults to None ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings plot_settings: dict or class plot_properties object or dict with any attribute of class plot_settings.Control the simulated curve

def plot_mesh( mesh: dict = None, coef_hydro: float = 99.0, catchment_polygon: None | pandas.DataFrame = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}):
840def plot_mesh(
841    mesh: dict = None,
842    coef_hydro: float = 99.0,
843    catchment_polygon: None | DataFrame = None,
844    ax_settings: dict | ax_properties = {},
845    fig_settings: dict | fig_properties = {},
846):
847    """
848    Plot the mesh of a smash model
849    Parameters:
850    -----------
851    mesh: a smash mesh as dictionary
852        a smash model object
853    coef_hydro: float
854        the coefficient to colorize the hydrographic network accodring the cumulative
855        surface. default is 99% so that 99% of the cell will be hidden.
856    ax_settings: dict or class ax_properties
857        object or dict with any attribute of class ax_properties
858    fig_settings: dict or class ax_properties
859        object or dict with any attribute of class fig_settings
860    """
861
862    if mesh is not None:
863        if isinstance(mesh, dict):
864            pass
865        else:
866            raise ValueError("mesh must be a dict")
867    else:
868        raise ValueError(
869            "model or mesh are mandatory and must be a dict or a smash Model object"
870        )
871
872    if isinstance(ax_settings, dict):
873        default_ax_settings = ax_properties(
874            title="Mesh of the Smash model",
875            xlabel="x_coords",
876            ylabel="y_coords",
877        )
878        default_ax_settings.update(**ax_settings)
879    else:
880        default_ax_settings = ax_properties(**ax_settings.__dict__)
881
882    if isinstance(fig_settings, dict):
883        fig_settings = fig_properties(**fig_settings)
884    else:
885        fig_settings = fig_properties(**fig_settings.__dict__)
886
887    # mesh["active_cell"]
888    gauge = mesh["gauge_pos"]
889    stations = mesh["code"]
890    flow_acc = mesh["flwacc"]
891    na = mesh["active_cell"] == 0
892
893    flow_accum_bv = np.where(na, 0.0, flow_acc.data / 1000000.0)
894    surfmin = (1.0 - coef_hydro / 100.0) * np.nanmax(flow_accum_bv)
895    mask_flow = flow_accum_bv < surfmin
896    flow_plot = np.where(mask_flow, np.nan, flow_accum_bv)
897    flow_plot = np.where(na, np.nan, flow_plot)
898
899    plt.rcParams.update(plt.rcParamsDefault)
900    fig, ax = plt.subplots()
901    fig, ax = default_ax_settings.change(figure=(fig, ax))
902
903    bbox = geo_toolbox.get_bbox_from_smash_mesh(mesh)
904    extent = (bbox["left"], bbox["right"], bbox["bottom"], bbox["top"])
905
906    active_cell = np.where(na, np.nan, mesh["active_cell"])
907    cmap = ListedColormap(["lightgray"])
908    ax.imshow(active_cell, cmap=cmap, extent=extent)
909
910    myblues = matplotlib.colormaps["Blues"]
911    cmp = ListedColormap(myblues(np.linspace(0.30, 1.0, 265)))
912    im = ax.imshow(flow_plot, cmap=cmp, extent=extent)
913
914    if catchment_polygon is not None:
915        # catchment_polygon = gpd.read_file(outlets_shapefile)
916        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
917
918    # create an axes on the right side of ax. The width of cax will be 5%
919    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
920    divider = make_axes_locatable(ax)
921    cax = divider.append_axes("right", size="5%", pad=0.05)
922
923    fig.colorbar(
924        im,
925        cmap="Blues",
926        ax=ax,
927        label="Cumulated surface (km²)",
928        shrink=0.75,
929        cax=cax,
930    )
931
932    pos_y = -5
933    ha = "right"
934    for i in range(len(stations)):
935        if pos_y > 0:
936            pos_y = -10
937        else:
938            pos_y = 5
939        # pos_y=-1*pos_y
940
941        if ha == "right":
942            ha = "left"
943            pos_x = 5
944        else:
945            ha = "right"
946            # pos_x = -5
947
948        coord = geo_toolbox.rowcol_to_xy(
949            gauge[i][0],
950            gauge[i][1],
951            mesh["xmin"],
952            mesh["ymax"],
953            mesh["xres"],
954            mesh["yres"],
955        ) + np.array(
956            [
957                mesh["dx"][gauge[i][0], gauge[i][1]] / 2,
958                -mesh["dx"][gauge[i][0], gauge[i][1]] / 2,
959            ]
960        )
961
962        code = stations[i]
963        ax.plot(coord[0], coord[1], color="green", marker="o", markersize=6)
964        ax.annotate(
965            code,  # this is the text
966            # these are the coordinates to position the label
967            (coord[0], coord[1]),
968            # textcoords="offset points",  # how to position the text
969            # xytext=(pos_x, pos_y),  # distance from text to points (x,y)
970            textcoords="data",  # how to position the text
971            xytext=(coord[0], coord[1]),  # distance from text to points (x,y)
972            ha=ha,  # horizontal alignment can be left, right or center
973            color="red",
974            fontsize=10,
975        )
976
977    fig, ax = default_ax_settings.change(figure=(fig, ax))
978
979    fig, ax = fig_settings.change(figure=(fig, ax))
980
981    return fig, ax

Plot the mesh of a smash model

Parameters:

mesh: a smash mesh as dictionary a smash model object coef_hydro: float the coefficient to colorize the hydrographic network accodring the cumulative surface. default is 99% so that 99% of the cell will be hidden. ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

def plot_xy_quantile( res_quantile, X, Y, res_quantile_obs=None, gauge_pos=None, figure=None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}, plot_settings: dict | plot_properties = {}):
 984def plot_xy_quantile(
 985    res_quantile,
 986    X,
 987    Y,
 988    res_quantile_obs=None,
 989    gauge_pos=None,
 990    figure=None,
 991    ax_settings: dict | ax_properties = {},
 992    fig_settings: dict | fig_properties = {},
 993    plot_settings: dict | plot_properties = {},
 994):
 995    """
 996    Plot the discharges quantiles fitting at X,Y coordinates.
 997    Parameters:
 998    -----------
 999    res_quantile: dict
1000        The result of the discharge quantile computation.
1001    res_quantile_obs: dict
1002        The results of the observed discharges quantile. res_quantile_obs is a dict and must be computed by the function smashbox.stats.stats.quantile_obs()
1003    gauge_pos: int
1004        gauge_pos is the index of gauge in the Smash mesh for which the quantile_discharge are provided to the function.
1005    X: int
1006        Coordinates of the pixel in the row directions (X means row)
1007    Y: int
1008        Coordinates of the pixel in the column directions (Y means column)
1009    figure: tuple
1010        input figure as (fig,ax) to add a new curve
1011    ax_settings: dict or class ax_properties
1012        object or dict with any attribute of class ax_properties
1013    fig_settings: dict or class ax_properties
1014        object or dict with any attribute of class fig_settings
1015    """
1016    if isinstance(ax_settings, dict):
1017        default_ax_settings = ax_properties(
1018            xscale="log",
1019            xlabel=f"Return period (*{res_quantile['chunk_size']} days)",
1020            ylabel="Discharges (m³/s)",
1021            grid=True,
1022            legend=True,
1023        )
1024        default_ax_settings.update(**ax_settings)
1025    else:
1026        default_ax_settings = ax_properties(**ax_settings.__dict__)
1027
1028    if isinstance(fig_settings, dict):
1029        fig_settings = fig_properties(**fig_settings)
1030    else:
1031        fig_settings = fig_properties(**fig_settings.__dict__)
1032
1033    if isinstance(plot_settings, dict):
1034        default_plot_settings = plot_properties(markersize=10)
1035        default_plot_settings.update(**plot_settings)
1036    else:
1037        default_plot_settings = plot_properties(**plot_settings.__dict__)
1038
1039    quantile = res_quantile["Q_th"][X, Y]
1040    maxima = res_quantile["maxima"][X, Y]
1041    T_emp = res_quantile["T_emp"]
1042    loc = res_quantile["fit_loc"][X, Y]
1043    scale = res_quantile["fit_scale"][X, Y]
1044    shape = res_quantile["fit_shape"][X, Y]
1045    fit = res_quantile["fit"]
1046
1047    sorted_data = np.sort(maxima)
1048
1049    plt.rcParams.update(plt.rcParamsDefault)
1050    if figure is None:
1051        fig, ax = plt.subplots()
1052    else:
1053        fig, ax = figure
1054
1055    fig, ax = default_ax_settings.change(figure=(fig, ax))
1056
1057    if res_quantile_obs is not None and len(res_quantile_obs.keys()) > 0:
1058        if gauge_pos is None:
1059            raise ValueError(
1060                "gauge_pos is None. gauge_pos argument must be an integer corresponding to the gauge index."
1061            )
1062        maxima_obs = res_quantile_obs["maxima"][gauge_pos, :]
1063        T_emp_obs = res_quantile_obs["Temp"][gauge_pos, :]
1064
1065        ax.plot(
1066            T_emp_obs,
1067            maxima_obs,
1068            "o",
1069            label="Observed",
1070            color="black",
1071            markersize=default_plot_settings.markersize,
1072        )
1073
1074    ax.plot(
1075        T_emp,
1076        sorted_data,
1077        "o",
1078        label="Empirical",
1079        markersize=default_plot_settings.markersize,
1080    )
1081
1082    ax.plot(
1083        res_quantile["T"],
1084        quantile,
1085        "x",
1086        label="Theorical",
1087        markersize=default_plot_settings.markersize,
1088    )
1089
1090    Trange = np.linspace(1.1, np.nanmax(res_quantile["T"]), 50)
1091
1092    if fit == "gumbel":
1093        ax.plot(
1094            Trange,
1095            [stats.quantile_gumbel(T, loc, scale) for T in Trange],
1096            "r--",
1097            label=f"{fit} fitted",
1098            lw=default_plot_settings.lw,
1099        )
1100
1101    if fit == "gev":
1102        ax.plot(
1103            Trange,
1104            [stats.quantile_gev(T, shape, loc, scale) for T in Trange],
1105            "r--",
1106            label=f"{fit} fitted",
1107            lw=default_plot_settings.lw,
1108        )
1109
1110    if "Umax" in res_quantile.keys() and "Umin" in res_quantile.keys():
1111        if (
1112            res_quantile["Umax"] is not None
1113            and res_quantile["Umin"] is not None
1114        ):
1115            ax.plot(
1116                res_quantile["T"],
1117                res_quantile["Umax"][X, Y],
1118                "r--",
1119                label="Uncertainties (max)",
1120                color="grey",
1121                lw=default_plot_settings.lw,
1122            )
1123            ax.plot(
1124                res_quantile["T"],
1125                res_quantile["Umin"][X, Y],
1126                "r--",
1127                label="Uncertainties (min)",
1128                color="grey",
1129                lw=default_plot_settings.lw,
1130            )
1131
1132    fig, ax = default_ax_settings.change(figure=(fig, ax))
1133    fig, ax = fig_settings.change(figure=(fig, ax))
1134
1135    return fig, ax

Plot the discharges quantiles fitting at X,Y coordinates.

Parameters:

res_quantile: dict The result of the discharge quantile computation. res_quantile_obs: dict The results of the observed discharges quantile. res_quantile_obs is a dict and must be computed by the function smashbox.stats.stats.quantile_obs() gauge_pos: int gauge_pos is the index of gauge in the Smash mesh for which the quantile_discharge are provided to the function. X: int Coordinates of the pixel in the row directions (X means row) Y: int Coordinates of the pixel in the column directions (Y means column) figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

def plot_image( matrice=array([[0., 0.], [0., 0.]]), bbox=None, vmin=None, vmax=None, mask=None, extend=None, catchment_polygon=None, figure=None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}):
1138def plot_image(
1139    matrice=np.zeros(shape=(2, 2)),
1140    bbox=None,
1141    vmin=None,
1142    vmax=None,
1143    mask=None,
1144    extend=None,
1145    catchment_polygon=None,
1146    figure=None,
1147    ax_settings: dict | ax_properties = {},
1148    fig_settings: dict | fig_properties = {},
1149):
1150    """
1151    Function for plotting a matrix as an image
1152
1153    Parameters
1154    ----------
1155    matrice : numpy array
1156        Matrix to be plotted
1157    bbox : list
1158        ["left","right","bottom","top"] bouding box to put x and y coordinates instead
1159    of the shape of the matrix
1160    vmin: real,
1161        minimum z value
1162    vmax: real,
1163        maximum z value
1164    mask: integer, matrix, shape of matice, contain 0 for pixels that should not be plotted
1165    catchment_polygon: dataframe containing some polygon to be plotted.
1166    Ideally it must contain the boundaries of the catchment as a polygon from a shp file
1167    read by geopanda.
1168    figure: tuple
1169        input figure as (fig,ax) to add a new curve
1170    ax_settings: dict or class ax_properties
1171        object or dict with any attribute of class ax_properties
1172    fig_settings: dict or class ax_properties
1173        object or dict with any attribute of class fig_settings
1174
1175    Examples
1176    ----------
1177    smash.utils.plot_image(mesh_france['drained_area'],bbox=bbox,title="Surfaces
1178                           drainƩes",xlabel="Longitude",ylabel="Latitude",zlabel="Surfaces drainƩes
1179                           km^2",vmin=0.0,vmax=1000,mask=mesh_france['global_active_cell'])
1180
1181    """
1182
1183    if isinstance(ax_settings, dict):
1184        ax_settings = ax_properties(**ax_settings)
1185    else:
1186        ax_settings = ax_properties(**ax_settings.__dict__)
1187
1188    if isinstance(fig_settings, dict):
1189        fig_settings = fig_properties(**fig_settings)
1190    else:
1191        fig_settings = fig_properties(**fig_settings.__dict__)
1192
1193    matrice = np.float32(matrice)
1194
1195    if bbox is not None:
1196        extent = [
1197            bbox["left"],
1198            bbox["right"],
1199            bbox["bottom"],
1200            bbox["top"],
1201        ]
1202    else:
1203        extent = None
1204
1205    if mask is not None:
1206        matrice[np.where(mask == 0)] = np.nan
1207
1208    plt.rcParams.update(plt.rcParamsDefault)
1209    if figure is None:
1210        fig, ax = plt.subplots()
1211    else:
1212        fig, ax = figure
1213
1214    if vmax is None:
1215        vmax = np.nanmax(matrice)
1216    if vmin is None:
1217        vmin = np.nanmin(matrice)
1218
1219    fig, ax = ax_settings.change(figure=(fig, ax))
1220
1221    # do it first otherwise crash if vmin>min de polygon
1222    if catchment_polygon is not None:
1223        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
1224
1225    im = ax.imshow(
1226        matrice, extent=extent, vmin=vmin, vmax=vmax, cmap=ax_settings.cmap
1227    )
1228
1229    # create an axes on the right side of ax. The width of cax will be 5%
1230    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1231    divider = make_axes_locatable(ax)
1232    cax = divider.append_axes("right", size="5%", pad=0.05)
1233
1234    plt.colorbar(im, label=ax_settings.clabel, cax=cax)
1235
1236    fig, ax = ax_settings.change(figure=(fig, ax))
1237    fig, ax = fig_settings.change(figure=(fig, ax))
1238
1239    return (fig, ax)

Function for plotting a matrix as an image

Parameters

matrice : numpy array Matrix to be plotted bbox : list ["left","right","bottom","top"] bouding box to put x and y coordinates instead of the shape of the matrix vmin: real, minimum z value vmax: real, maximum z value mask: integer, matrix, shape of matice, contain 0 for pixels that should not be plotted catchment_polygon: dataframe containing some polygon to be plotted. Ideally it must contain the boundaries of the catchment as a polygon from a shp file read by geopanda. figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

Examples

smash.utils.plot_image(mesh_france['drained_area'],bbox=bbox,title="Surfaces drainƩes",xlabel="Longitude",ylabel="Latitude",zlabel="Surfaces drainƩes km^2",vmin=0.0,vmax=1000,mask=mesh_france['global_active_cell'])

def plot_misfit( values: numpy.ndarray = [], names: numpy.ndarray = [], columns: list | None = None, misfit: str = 'nse', figure: list | tuple | None = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}):
1242def plot_misfit(
1243    values: np.ndarray = [],
1244    names: np.ndarray = [],
1245    columns: list | None = None,
1246    misfit: str = "nse",
1247    figure: list | tuple | None = None,
1248    ax_settings: dict | ax_properties = {},
1249    fig_settings: dict | fig_properties = {},
1250):
1251    """
1252    Plot the misfit criteria between the simulated and observed discharges.
1253    Parameters:
1254    -----------
1255    values: np.ndarray
1256        The result of the discharge misfit for all outlets.
1257    names: np.ndarray
1258        Outlets name or code stored in an np.ndarray.
1259    columns: list | None
1260        Columns of the np.ndarray to plot
1261    misfit: str
1262        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
1263    figure: tuple
1264        input figure as (fig,ax) to add a new curve
1265    ax_settings: dict or class ax_properties
1266        object or dict with any attribute of class ax_properties
1267    fig_settings: dict or class ax_properties
1268        object or dict with any attribute of class fig_settings
1269    """
1270
1271    if isinstance(ax_settings, dict):
1272        default_ax_settings = ax_properties(
1273            ylabel=f"{misfit} criteria",
1274            xlabel="Gauges stations",
1275            grid=True,
1276            legend=True,
1277            xticklabels_rotation=45,
1278            xtics_fontsize=8,
1279        )
1280        default_ax_settings.update(**ax_settings)
1281    else:
1282        default_ax_settings = ax_properties(**ax_settings.__dict__)
1283
1284    if isinstance(fig_settings, dict):
1285        fig_settings = fig_properties(**fig_settings)
1286    else:
1287        fig_settings = fig_properties(**fig_settings.__dict__)
1288
1289    if len(names) == 0:
1290        names = np.arange(len(values))
1291
1292    if columns is not None:
1293        values = values[columns]
1294        names = names[columns]
1295
1296    # remove nan from plot
1297    columns = list(np.isnan(values) == False)
1298    # print(columns)
1299    if len(columns) > 0:
1300        values = values[columns]
1301        names = names[columns]
1302
1303    if figure is None:
1304        fig, ax = plt.subplots()
1305    else:
1306        fig, ax = figure
1307
1308    fig, ax = default_ax_settings.change(figure=(fig, ax))
1309    bar_container = ax.bar(names, values, color="grey", tick_label=names)
1310
1311    ax.bar_label(
1312        bar_container,
1313        fmt=lambda x: f"{x:.2f}",
1314        fontsize=default_ax_settings.barlabel_fontsize,
1315    )
1316
1317    fig, ax = default_ax_settings.change(figure=(fig, ax))
1318    fig, ax = fig_settings.change(figure=(fig, ax))
1319
1320    return fig, ax

Plot the misfit criteria between the simulated and observed discharges.

Parameters:

values: np.ndarray The result of the discharge misfit for all outlets. names: np.ndarray Outlets name or code stored in an np.ndarray. columns: list | None Columns of the np.ndarray to plot misfit: str Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']" figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

def plot_outlet_stats( values_sim: numpy.ndarray | None = None, values_obs: numpy.ndarray | None = None, names: numpy.ndarray = [], columns: list | None = [], stat: str = 'max', figure: list | tuple | None = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}):
1323def plot_outlet_stats(
1324    values_sim: np.ndarray | None = None,
1325    values_obs: np.ndarray | None = None,
1326    names: np.ndarray = [],
1327    columns: list | None = [],
1328    stat: str = "max",
1329    figure: list | tuple | None = None,
1330    ax_settings: dict | ax_properties = {},
1331    fig_settings: dict | fig_properties = {},
1332):
1333    """
1334    Plot a statistical criteria at a given list of outlet.
1335    Parameters:
1336    -----------
1337    values_sim: np.ndarray or None
1338        The result of the simulated stat for all outlets.
1339    values_obs: np.ndarray or None
1340        The result of the observed stat for all outlets.
1341    names: np.ndarray
1342        Outlets name or code stored in an np.ndarray.
1343    columns: list | None
1344        Columns of the np.ndarray to plot
1345    stat: str
1346        Criteria to plot. choice are ['max', 'min', 'mean', 'median', 'q20', 'q80']"
1347    figure: tuple
1348        input figure as (fig,ax) to add a new curve
1349    ax_settings: dict or class ax_properties
1350        object or dict with any attribute of class ax_properties
1351    fig_settings: dict or class ax_properties
1352        object or dict with any attribute of class fig_settings
1353    """
1354
1355    if isinstance(ax_settings, dict):
1356        default_ax_settings = ax_properties(
1357            ylabel=f"{stat} discharges (m3/s)",
1358            xlabel="Gauges stations",
1359            grid=True,
1360            legend=True,
1361            xticklabels_rotation=45,
1362            xtics_fontsize=6,
1363        )
1364        default_ax_settings.update(**ax_settings)
1365    else:
1366        default_ax_settings = ax_properties(**ax_settings.__dict__)
1367
1368    if isinstance(fig_settings, dict):
1369        fig_settings = fig_properties(**fig_settings)
1370    else:
1371        fig_settings = fig_properties(**fig_settings.__dict__)
1372
1373    if columns is not None:
1374        if values_sim is not None:
1375            values_sim = values_sim[columns]
1376
1377        if values_obs is not None:
1378            values_obs = values_obs[columns]
1379
1380        names = names[columns]
1381
1382    if np.all(values_obs == -99.0):
1383        values_obs = None
1384
1385    if values_sim is not None and values_obs is not None:
1386        if values_obs.size != values_sim.size:
1387            raise ValueError(
1388                "values_sim and values_obs must have the same size !"
1389            )
1390
1391    if figure is None:
1392        fig, ax = plt.subplots()
1393    else:
1394        fig, ax = figure
1395
1396    fig, ax = default_ax_settings.change(figure=(fig, ax))
1397
1398    x = np.arange(len(names))
1399    width = 0.25  # the width of the bars
1400
1401    multiplier = 0
1402
1403    if values_sim is not None:
1404        offset = width * multiplier
1405        ax.bar(x + offset, values_sim, width, label="obs")
1406        multiplier += 1
1407        # ax.bar_label(rects, padding=3)
1408
1409    if values_obs is not None:
1410        offset = width * multiplier
1411        ax.bar(x + offset, values_obs, width, label="sim")
1412        # ax.bar_label(rects, padding=3)
1413        # multiplier += 1
1414
1415    ax.set_xticks(x + width, names)
1416
1417    # bar_container = ax.bar(names, values, color="grey", tick_label=names)
1418
1419    # ax.bar_label(
1420    #     bar_container,
1421    #     fmt=lambda x: f"{x:.2f}",
1422    #     fontsize=default_ax_settings.barlabel_fontsize,
1423    # )
1424
1425    fig, ax = default_ax_settings.change(figure=(fig, ax))
1426    fig, ax = fig_settings.change(figure=(fig, ax))
1427
1428    return fig, ax

Plot a statistical criteria at a given list of outlet.

Parameters:

values_sim: np.ndarray or None The result of the simulated stat for all outlets. values_obs: np.ndarray or None The result of the observed stat for all outlets. names: np.ndarray Outlets name or code stored in an np.ndarray. columns: list | None Columns of the np.ndarray to plot stat: str Criteria to plot. choice are ['max', 'min', 'mean', 'median', 'q20', 'q80']" figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings

def plot_misfit_map( values: numpy.ndarray = [], names: numpy.ndarray = [], mesh=None, misfit: str = 'nse', coef_hydro=99.0, catchment_polygon: None | pandas.DataFrame = None, ax_settings: dict | ax_properties = {}, fig_settings: dict | fig_properties = {}, plot_settings: dict | plot_properties = {}):
1431def plot_misfit_map(
1432    values: np.ndarray = [],
1433    names: np.ndarray = [],
1434    mesh=None,
1435    misfit: str = "nse",
1436    coef_hydro=99.0,
1437    catchment_polygon: None | DataFrame = None,
1438    ax_settings: dict | ax_properties = {},
1439    fig_settings: dict | fig_properties = {},
1440    plot_settings: dict | plot_properties = {},
1441):
1442    """
1443    Map plot of the misfit criteria between the simulated and observed discharges.
1444    Parameters:
1445    -----------
1446    values: np.ndarray
1447        The result of the discharge misfit for all outlets.
1448    names: np.ndarray
1449        Outlets name or code stored in an np.ndarray.
1450    mesh: None | dict
1451        The mesh of the Smash model as dict
1452    misfit: str
1453        Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']"
1454    figure: tuple
1455        input figure as (fig,ax) to add a new curve
1456    ax_settings: dict or class ax_properties
1457        object or dict with any attribute of class ax_properties
1458    fig_settings: dict or class ax_properties
1459        object or dict with any attribute of class fig_settings
1460    plot_settings_sim: dict or class ax_properties
1461        object or dict with any attribute of class plot_settings.
1462    """
1463    if mesh is not None:
1464        if isinstance(mesh, dict):
1465            pass
1466        else:
1467            raise ValueError("mesh must be a dict")
1468    else:
1469        raise ValueError(
1470            "model or mesh are mandatory and must be a dict or a smash Model object"
1471        )
1472
1473    if isinstance(ax_settings, dict):
1474        default_ax_settings = ax_properties(
1475            title=f"Map of {misfit} criteria over the domain.",
1476            xlabel="x_coords",
1477            ylabel="y_coords",
1478            cmap="turbo_r",
1479        )
1480        default_ax_settings.update(**ax_settings)
1481    else:
1482        default_ax_settings = ax_properties(**ax_settings.__dict__)
1483
1484    if isinstance(fig_settings, dict):
1485        fig_settings = fig_properties(**fig_settings)
1486    else:
1487        fig_settings = fig_properties(**fig_settings.__dict__)
1488
1489    if isinstance(plot_settings, dict):
1490        default_plot_settings = plot_properties(
1491            marker="o",
1492            markersize=8,
1493        )
1494        default_plot_settings.update(**plot_settings)
1495    else:
1496        default_plot_settings = plot_properties(**plot_settings.__dict__)
1497
1498    # unset attribute color, managed separatly
1499    delattr(default_plot_settings, "color")
1500
1501    gauge = mesh["gauge_pos"]
1502    stations = mesh["code"]
1503    flow_acc = mesh["flwacc"]
1504    na = mesh["active_cell"] == 0
1505
1506    bbox = geo_toolbox.get_bbox_from_smash_mesh(mesh)
1507    extent = (bbox["left"], bbox["right"], bbox["bottom"], bbox["top"])
1508
1509    flow_accum_bv = np.where(na, 0.0, flow_acc.data)
1510    surfmin = (1.0 - coef_hydro / 100.0) * np.nanmax(flow_accum_bv)
1511    mask_flow = flow_accum_bv < surfmin
1512    flow_plot = np.where(mask_flow, np.nan, flow_accum_bv.data)
1513    flow_plot = np.where(na, np.nan, flow_plot)
1514
1515    plt.rcParams.update(plt.rcParamsDefault)
1516    fig, ax = plt.subplots()
1517    fig, ax = default_ax_settings.change(figure=(fig, ax))
1518
1519    active_cell = np.where(na, np.nan, mesh["active_cell"])
1520    cmap = ListedColormap(["lightgray"])
1521    ax.imshow(active_cell, cmap=cmap, extent=extent)
1522
1523    myblues = matplotlib.colormaps["binary"]
1524    cmp = ListedColormap(myblues(np.linspace(0.20, 1.0, 265)))
1525    im = ax.imshow(flow_plot, cmap=cmp, extent=extent)
1526
1527    if catchment_polygon is not None:
1528        # catchment_polygon = gpd.read_file(outlets_shapefile)
1529        catchment_polygon.plot(ax=ax, facecolor="none", edgecolor="black")
1530
1531    # create an axes on the right side of ax. The width of cax will be 5%
1532    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1533    divider = make_axes_locatable(ax)
1534    cax = divider.append_axes("right", size="5%", pad=0.05)
1535
1536    fig.colorbar(
1537        im,
1538        cmap="Blues",
1539        ax=ax,
1540        label="Cumulated surface (km²)",
1541        shrink=0.75,
1542        cax=cax,
1543    )
1544
1545    # define bounds for the colormap
1546    if misfit == "nse" or misfit == "nnse":
1547        vmin = 0
1548        vmax = 1
1549    elif misfit == "rmse" or misfit == "nrmse" or misfit == "se":
1550        vmin = 0
1551        vmax = np.nanmax(values)
1552    else:
1553        vmin = np.nanmin(values)
1554        vmax = np.nanmax(values)
1555
1556    colormap = cm.get_cmap(default_ax_settings.cmap)
1557    cmp = ListedColormap(colormap(np.linspace(vmin, vmax, 256)))
1558
1559    ha = "right"
1560    for i in range(len(stations)):
1561
1562        if ha == "right":
1563            ha = "left"
1564            str_val = str(np.round(values[i], 2)).rjust(int(len(stations[i])))
1565            code = f"{stations[i]}\n {str_val}"
1566
1567        else:
1568            ha = "right"
1569            str_val = str(np.round(values[i], 2)).ljust(int(len(stations[i])))
1570            code = f"{stations[i]}\n {str_val}"
1571
1572        coord = geo_toolbox.rowcol_to_xy(
1573            gauge[i][0],
1574            gauge[i][1],
1575            mesh["xmin"],
1576            mesh["ymax"],
1577            mesh["xres"],
1578            mesh["yres"],
1579        )
1580
1581        ax.plot(
1582            coord[0],
1583            coord[1],
1584            color=cmp(values[i]),
1585            **default_plot_settings.__dict__,
1586        )
1587
1588        ax.annotate(
1589            code,  # this is the text
1590            # these are the coordinates to position the label
1591            (coord[0], coord[1]),
1592            textcoords="data",  # how to position the text
1593            xytext=(coord[0], coord[1]),  # distance from text to points (x,y)
1594            ha=ha,  # horizontal alignment can be left, right or center
1595            color=cmp(values[i]),
1596            fontsize=default_ax_settings.annotate_fontsize
1597            * default_ax_settings.font_ratio,
1598        )
1599
1600    import matplotlib as mpl
1601
1602    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
1603    # create an axes on the right side of ax. The width of cax will be 5%
1604    # of ax and the padding between cax and ax will be fixed at 0.05 inch.
1605    # divider = make_axes_locatable(ax)
1606    cax = divider.append_axes("right", size="5%", pad=0.5)
1607
1608    fig.colorbar(
1609        cm.ScalarMappable(norm=norm, cmap=cmp),
1610        cmap=cmp,
1611        ax=ax,
1612        cax=cax,
1613        label=misfit,
1614        shrink=0.75,
1615        location="right",
1616    )
1617
1618    fig, ax = default_ax_settings.change(figure=(fig, ax))
1619
1620    fig, ax = fig_settings.change(figure=(fig, ax))
1621
1622    return fig, ax

Map plot of the misfit criteria between the simulated and observed discharges.

Parameters:

values: np.ndarray The result of the discharge misfit for all outlets. names: np.ndarray Outlets name or code stored in an np.ndarray. mesh: None | dict The mesh of the Smash model as dict misfit: str Criteria to plot. choice are ['nse', 'nnse', 'rmse', 'nrmse', 'se', 'kge']" figure: tuple input figure as (fig,ax) to add a new curve ax_settings: dict or class ax_properties object or dict with any attribute of class ax_properties fig_settings: dict or class ax_properties object or dict with any attribute of class fig_settings plot_settings_sim: dict or class ax_properties object or dict with any attribute of class plot_settings.