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
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.
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)"""
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.
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"
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)
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)
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.
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)"""
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)
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)
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
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
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
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
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
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
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
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
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'])
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
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
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.