smashbox.model.model
1import os 2import smash 3 4# import pyhdf5_handler 5# from pyhdf5_handler.src import hdf5_handler 6import numpy as np 7import pandas as pd 8import datetime 9 10# from smashbox.src import param 11from smashbox.model import setup 12from smashbox.model import mesh 13from smashbox.plot import myplot 14from smashbox.tools import geo_toolbox 15from smashbox.tools import tools 16from smashbox.model import atmos_data_connector 17from smashbox.stats import mystats 18from smashbox.read_inputdata import smashmodel 19 20import pyhdf5_handler 21import copy 22 23 24class model: 25 """Main class model() which has a complete set of functions, class and attributes to 26 build, run and manipulate the Smash model, compute statistical criterium and 27 plot graphics.""" 28 29 def __init__(self, name, myparam): 30 31 self._model_name = name 32 33 self._myparam = copy.deepcopy(myparam) 34 """The attribute _myparam is a copy of the parent class smashbox.myparam. 35 This class stores the main parameters used for building the Smash model""" 36 37 self.mysetup = setup.setup(self._myparam.param) 38 """The attribute mysetup own the class setup.setup(). This class stores the Smash 39 setup for the hydrological simulation and some helpers to manipulate these 40 parameters.""" 41 42 self.mymesh = mesh.mesh(self.mysetup) 43 """The attribute mymesh own the class mesh.mesh(). This class stores the Smash 44 mesh used for the hydrological simulation and some helpers to manipulate this mesh. 45 """ 46 47 self.mysmashmodel = None 48 """The attribute mysmashmodel stores the Smash model object created with 49 attributes mysetup and -mymesh.""" 50 51 self._fstates = None 52 self._istates = None 53 self._myatmos_data_connector = None 54 55 self.warmup_model = None 56 """The attribute warmup_model store a smash model used for warmup and compatible 57 with the model in attribute mysmashmodel""" 58 59 self.optimize_model = None 60 """The attribute optimize_model store a smash model used for optimization and compatible 61 with the model in attribute mysmashmodel""" 62 63 self.extra_smash_results = None 64 """The attribute extra_smash_results stores the extra results provided by Smash 65 if the parameter return_option is defined.""" 66 67 self.mystats = mystats.mystats(self) 68 """The attribute mystats own the class mystats.mystats(). This class stores 69 statistical functions and results which could be applied on the smash model.""" 70 71 self.myplot = myplot.myplot(self) 72 """The attribute myplot own the class myplot.myplot(). This class stores plotting 73 capabilities on the smash model object.""" 74 75 def generate_mesh( 76 self, 77 max_depth: float = 1.0, 78 query: str | None = None, 79 area_error_th: None | float = None, 80 lacuna_threshold: None | float = None, 81 ): 82 """ 83 Generate the mesh of the Smash model 84 85 Parameters 86 ---------- 87 88 max_depth : `int`, default 1 89 The maximum depth accepted by the algorithm to find the catchment outlet. 90 A **max_depth** of 1 means that the algorithm will search among the 91 combinations in 92 (``row - 1``, ``row``, ``row + 1``; ``col - 1``, ``col``, ``col + 1``), 93 the coordinates that minimize 94 the relative error between the given catchment area and the modeled 95 catchment area calculated from the 96 flow directions file. 97 :param query: Any pandas dataframe query as string: '(SURF>20) & (SURF<100)'. This query 98 must be build using the field (column name) in the outlet database. 99 https://pandas.pydata.org/docs/user_guide/indexing.html#the-query-method 100 :type query: str 101 area_error_th: float | None 102 The tolerance error for the difference between the observed and simulated 103 surface. The error is computed as follow: 104 Serror=abs(Ssim-Sobs)/Sobs 105 All outlets where `Serror > area_error_th` will be automatically removed from 106 the mesh. 107 :type area_error_th: float 108 :param lacuna_threshold: Lacuna threshold for the discharges in percent. All gauge where the proportion of lacuna exceed this threshold will be removed. 109 :type: float | Nonetype 110 111 Examples 112 -------- 113 114 >>> es=smashbox.SmashBox() 115 >>> sb.newmodel("RealCollobrier") 116 >>> sb.RealCollobrier.generate_mesh(min_surf=5, max_surf=100) 117 118 """ 119 self.mymesh.generate_mesh( 120 self._myparam.param, 121 max_depth=max_depth, 122 query=query, 123 area_error_th=area_error_th, 124 lacuna_threshold=lacuna_threshold, 125 ) 126 127 def model(self, setup: dict | None = None, mesh: dict | None = None, read_data=None): 128 """ 129 Smash model object creation. This function wrap smash.Model(). Setup and mesh 130 argument are optional since these dictionnary are hosted by the smashbox object. 131 132 Parameters 133 ---------- 134 setup: dict | None 135 The Smash setup (optionnal), if None the smashbox setup will be used 136 mesh: dict | None 137 The Smash mesh (optionnal), if None the smashbox mesh will be used 138 139 Examples 140 -------- 141 142 >>> es=smashbox.SmashBox() 143 >>> sb.newmodel("RealCollobrier") 144 >>> sb.RealCollobrier.generate_mesh(run=False) 145 >>> sb.RealCollobrier.model() 146 147 """ 148 if setup is None: 149 setup = self.mysetup.setup.copy() 150 151 if mesh is None: 152 mesh = self.mymesh.mesh.copy() 153 154 if read_data is False: 155 setup.update( 156 { 157 "read_prcp": False, 158 "read_pet": False, 159 "read_snow": False, 160 "read_qobs": False, 161 "read_temp": False, 162 } 163 ) 164 if read_data is True: 165 setup.update( 166 { 167 "read_prcp": True, 168 "read_pet": True, 169 "read_snow": True, 170 "read_qobs": True, 171 "read_temp": True, 172 } 173 ) 174 175 self.mysmashmodel = self._model(setup=setup, mesh=mesh) 176 177 if read_data is True: 178 self._gathering_atmosdata() 179 180 self._gathering_parameters(self.mysmashmodel) 181 182 def _model(self, setup=None, mesh=None): 183 """ 184 Smash model object creation. This function wrap smash.Model() 185 186 Parameters 187 ---------- 188 setup: dict | None 189 The Smash setup (optionnal), if None the smashbox setup will be used 190 mesh: dict | None 191 The Smash mesh (optionnal), if None the smashbox mesh will be used 192 193 Examples 194 -------- 195 196 >>> es=smashbox.SmashBox() 197 >>> sb.newmodel("RealCollobrier") 198 >>> sb.RealCollobrier.generate_mesh(run=False) 199 >>> sb.RealCollobrier.model() 200 201 """ 202 if setup is None: 203 print("</> The input setup is None ") 204 return None 205 206 if mesh is None: 207 print("</> input mesh is None. use smashbox.model.model.generate_mesh()") 208 return None 209 210 if self._myparam.param.enhanced_smash_input_data: 211 model = smashmodel.SmashModel(setup, mesh) 212 else: 213 model = smash.Model(setup, mesh) 214 215 return model 216 217 def _gathering_parameters(self, model): 218 219 if self.optimize_model is not None: 220 print(f"</> Getting parameters from previously optimized model ...") 221 model.rr_parameters = self.optimize_model.rr_parameters 222 else: 223 print( 224 f"</> Importing parameters from {self._myparam.param._smash_parameters} ..." 225 ) 226 self.import_parameters(model=model) 227 self._transform_parameters( 228 model=model, dt_origin=self._myparam.param._smash_parameters_dt 229 ) 230 231 def _transform_parameters(self, model=None, dt_origin: None | float = None): 232 """ 233 Function to transform the parameters according the model time-step and the original 234 time-step used for calibrated the parameters. 235 :param dt_origin: Original time-step used for generate the calibrated parameter, 236 defaults to None 237 :type dt_origin: None | float, optional 238 239 """ 240 # if dt_origin is None: 241 # raise ValueError( 242 # " Argument dt_origin is None. This must be filled with the value " 243 # "of the timestep used to calibrate the parameters." 244 # ) 245 246 # if not hasattr(self, "mysmashmodel") or self.mysmashmodel is None: 247 # raise ValueError( 248 # "</> mysmashmodel attribute does not exist or is None. " 249 # "The model must be created first..." 250 # ) 251 if model is None: 252 return 253 254 dt_target = model.setup.dt 255 256 if dt_origin is None: 257 return 258 259 if dt_target == dt_origin: 260 return 261 262 print( 263 f"</> Tranforming parameters `ct`, `kexec`, `llr` calibrated with a time-step " 264 f"of {dt_origin}s to the modeled time-step {dt_target}s." 265 ) 266 parameters_tranfsorm = ["ct", "kexc", "llr"] 267 power_tranform = [1.0 / 4.0, -1.0 / 8.0, 1.0] 268 269 for i, param in enumerate(parameters_tranfsorm): 270 index_param = np.where(param == model.rr_parameters.keys)[0] 271 if len(index_param) > 0: 272 model.rr_parameters.values[:, :, index_param[0]] = ( 273 model.rr_parameters.values[:, :, index_param[0]] 274 * (dt_origin / dt_target) ** power_tranform[i] 275 ) 276 277 def _gathering_atmosdata(self): 278 279 if self._myatmos_data_connector is not None: 280 281 print("</> Gathering atmos data ...") 282 283 if ( 284 self._myatmos_data_connector.input_ntimestep 285 != self.mysmashmodel.setup.ntime_step 286 ): 287 print( 288 "</> Warnings: Inconsistant ntime_step " 289 f"{self._myatmos_data_connector.input_ntimestep}" 290 f"!={self.mysmashmodel.setup.ntime_step}, " 291 "the Smash model will be rebuild." 292 ) 293 294 self._model() 295 self._gathering_parameters(self.mysmashmodel) 296 297 if self._myatmos_data_connector.smash_prcp is not None: 298 self.mysmashmodel.atmos_data.prcp = ( 299 self._myatmos_data_connector.smash_prcp 300 ) 301 302 if self._myatmos_data_connector.smash_pet is not None: 303 self.mysmashmodel.atmos_data.pet = self._myatmos_data_connector.smash_pet 304 305 if self.mysmashmodel.setup.prcp_partitioning: 306 print("</> Compute prcp partitionning ...") 307 smash.fcore._mw_atmos_statistic.compute_prcp_partitioning( 308 model.setup, model.mesh, model._input_data 309 ) 310 311 if self.mysmashmodel.setup.compute_mean_atmos: 312 print("</> Computing mean atmospheric data") 313 smash.fcore._mw_atmos_statistic.compute_mean_atmos( 314 self.mysmashmodel.setup, 315 self.mysmashmodel.mesh, 316 self.mysmashmodel._input_data, 317 ) 318 319 @tools.autocast_args 320 def model_warmup(self, warmup: int = 365): 321 """ 322 Smash model warmup function. This function warm the curent model by creating a new 323 on with attribute warmup_model. The final states of warmup_model are copied to the 324 initial states of mysmashmodel. 325 326 Parameters 327 ---------- 328 329 warmup: None | int 330 a integer of the number of days used for warming the model. 331 332 """ 333 print("</> Warmup the Smash-model...") 334 335 if warmup is not None: 336 try: 337 timedelta = datetime.timedelta(days=warmup) 338 except: 339 raise ValueError(f"warmup arg `{warmup}` is not a valid time delta.") 340 341 w_setup = copy.deepcopy(self.mysetup.setup) 342 343 w_setup["end_time"] = w_setup["start_time"] 344 w_setup["start_time"] = datetime.datetime.strftime( 345 datetime.datetime.fromisoformat(w_setup["start_time"]) - timedelta, 346 "%Y-%m-%d %H:%M", 347 ) 348 w_setup["read_qobs"] = False 349 w_setup["read_prcp"] = True 350 w_setup["read_pet"] = True 351 352 if self.warmup_model is None: 353 self.warmup_model = self._model(setup=w_setup, mesh=self.mymesh.mesh) 354 355 self._gathering_parameters(self.warmup_model) 356 self.warmup_model.forward_run() 357 358 # TODO FIX BUG IN SMASH when states > 1.0 359 mask_sup = np.where(self.warmup_model.rr_final_states.values > 1) 360 self.warmup_model.rr_final_states.values[mask_sup] = 0.9999 361 362 if hasattr(self, "mysmashmodel") and self.mysmashmodel is not None: 363 364 self.mysmashmodel.rr_initial_states = self.warmup_model.rr_final_states 365 366 def optimize( 367 self, 368 start_time: None | str = None, 369 end_time: None | str = None, 370 mapping: str = "uniform", 371 optimizer: None | str = None, 372 optimize_options: None | str = None, 373 cost_options: None | str = None, 374 common_options: None | str = None, 375 return_options: None | str = None, 376 callback=None, 377 ): 378 """ 379 Optimize the current model (with the current setup and mesh), 380 store the model in the attribute optimize_model and set the calibrated 381 parameters to the model behind the attribute mysmashmodel. 382 Start_time and end_time can be specified here to change the period of the 383 calibration compare to the current setup. All other arguments are 384 equivalent to the smash.model.optimize function 385 (see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize). 386 One difference from Smash is that gauges with no data are 387 automatically removed from the optimization without raising an error. 388 This provide a convient way to calibrate quickly the parameters using the current 389 mesh which may include gauges with data for calibration 390 and location gauge for discharges computation. 391 392 :param start_time: The start time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 393 :type start_time: None | str, optional 394 :param end_time: The end time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 395 :type end_time: None | str, optional 396 :param mapping: Type of mapping, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to "uniform" 397 :type mapping: str, optional 398 :param optimizer: Name of optimizer, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 399 :type optimizer: None | str, optional 400 :param optimize_options: Dictionary containing optimization options for fine-tuning the optimization process, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 401 :type optimize_options: None | str, optional 402 :param cost_options: Dictionary containing computation cost options for simulated and observed responses. see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 403 :type cost_options: None | str, optional 404 :param common_options: Dictionary containing common options with two elements: ncpu (int, default 1) and verbose (bool, default is False), defaults to None 405 :type common_options: None | str, optional 406 :param return_options: Dictionary containing return options to save additional simulation results, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 407 :type return_options: None | str, optional 408 :param callback: A callable called after each iteration with the signature callback(iopt: Optimize), see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 409 :type callback: TYPE, optional 410 411 """ 412 o_setup = copy.deepcopy(self.mysetup.setup) 413 414 if start_time is not None and end_time is not None: 415 o_setup["start_time"] = start_time 416 o_setup["end_time"] = end_time 417 418 o_setup["read_qobs"] = True 419 o_setup["read_prcp"] = True 420 o_setup["read_pet"] = True 421 422 if self.optimize_model is None: 423 self.optimize_model = self._model(setup=o_setup, mesh=self.mymesh.mesh) 424 print(f"</> Getting parameters from the default model `mysmashmodel` ...") 425 426 for key in list(self.optimize_model.rr_parameters.keys): 427 pos = list(self.optimize_model.rr_parameters.keys).index(key) 428 pos_w = list(self.mysmashmodel.rr_parameters.keys).index(key) 429 self.optimize_model.rr_parameters.values[:, :, pos] = ( 430 self.mysmashmodel.rr_parameters.values[:, :, pos_w] 431 ) 432 # ~ self.optimize_model.rr_parameters = self.mysmashmodel.rr_parameters.copy() 433 434 if self.warmup_model is not None: 435 for key in list(self.optimize_model.rr_initial_states.keys): 436 pos = list(self.optimize_model.rr_initial_states.keys).index(key) 437 pos_w = list(self.warmup_model.rr_initial_states.keys).index(key) 438 self.optimize_model.rr_initial_states.values[:, :, pos] = ( 439 self.warmup_model.rr_final_states.values[:, :, pos_w] 440 ) 441 442 default_cost_option = {"end_warmup": o_setup["start_time"], "gauge": "all"} 443 if cost_options is not None: 444 default_cost_option.update(cost_options) 445 446 # auto remove gauge if no observation 447 if isinstance(default_cost_option["gauge"], str): 448 if default_cost_option["gauge"] == "dws": 449 gauge = np.empty(shape=0) 450 451 for i, pos in enumerate(self.optimize_model.mesh.gauge_pos): 452 if self.optimize_model.mesh.flwdst[tuple(pos)] == 0: 453 gauge = np.append(gauge, self.optimize_model.mesh.code[i]) 454 455 elif default_cost_option["gauge"] == "all": 456 gauge = np.array(self.optimize_model.mesh.code, ndmin=1) 457 else: 458 gauge = np.array(default_cost_option["gauge"], ndmin=1) 459 elif isinstance(default_cost_option["gauge"], list): 460 gauge = np.array(default_cost_option["gauge"], ndmin=1) 461 462 st = pd.Timestamp(self.optimize_model.setup.start_time) 463 et = pd.Timestamp(self.optimize_model.setup.end_time) 464 ew = pd.Timestamp(default_cost_option["end_warmup"]) 465 start_slice = int((ew - st).total_seconds() / self.optimize_model.setup.dt) 466 end_slice = int((et - ew).total_seconds() / self.optimize_model.setup.dt) 467 time_slice = slice(start_slice, end_slice) 468 469 del_gauge = [] 470 for i in range(len(gauge)): 471 472 if np.all(self.optimize_model.response_data.q[i, time_slice] < 0): 473 del_gauge.append(i) 474 475 print( 476 f"No observed discharge available at gauge '{gauge[i]}' for the selected " 477 f"optimization period ['{ew}', '{et}']. This gauge is removed " 478 f"from the optimization." 479 ) 480 481 gauge = np.delete(gauge, del_gauge) 482 default_cost_option.update({"gauge": gauge}) 483 484 self.optimize_model.optimize( 485 mapping, 486 optimizer, 487 optimize_options, 488 default_cost_option, 489 common_options, 490 return_options, 491 callback, 492 ) 493 494 if hasattr(self, "mysmashmodel") and self.mysmashmodel is not None: 495 self.mysmashmodel.rr_parameters = self.optimize_model.rr_parameters.copy() 496 497 @tools.autocast_args 498 def forward_run( 499 self, 500 warmup: int | None = None, 501 invert_states: bool | None = False, 502 cost_options: dict | None = None, 503 common_options: dict | None = None, 504 return_options: dict | None = None, 505 ): 506 """ 507 Smash model forward run.This function wrap smash.model.forward_run(). 508 509 Parameters 510 ---------- 511 512 warmup: None | int 513 a integer of the number of days used for warming the model. 514 invert_states : bool = False 515 invert states of the model, so that the final states are used 516 for the initial states. 517 cost_options : dict | None, 518 Dictionary containing computation cost options for simulated and observed responses (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html). 519 common_options: dict| None, 520 Dictionary containing common options with two elements, ncpu and verbose (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html) 521 return_options : dict | None, 522 Dictionary containing return options to save additional simulation results. (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html) 523 524 Examples 525 -------- 526 527 >>> es=smashbox.SmashBox() 528 >>> sb.newmodel("RealCollobrier") 529 >>> sb.RealCollobrier.generate_mesh(run=False) 530 >>> sb.RealCollobrier.model() 531 >>> sb.RealCollobrier.forward_run() 532 """ 533 534 if self.mysmashmodel is None: 535 self.model() 536 537 if warmup is not None: 538 self.model_warmup(warmup=warmup) 539 540 if self.warmup_model is not None: 541 for key in list(self.mysmashmodel.rr_initial_states.keys): 542 pos = list(self.mysmashmodel.rr_initial_states.keys).index(key) 543 pos_w = list(self.warmup_model.rr_initial_states.keys).index(key) 544 self.mysmashmodel.rr_initial_states.values[:, :, pos] = ( 545 self.warmup_model.rr_final_states.values[:, :, pos_w] 546 ) 547 548 if self.optimize_model is not None: 549 for key in list(self.mysmashmodel.rr_parameters.keys): 550 pos = list(self.mysmashmodel.rr_parameters.keys).index(key) 551 pos_w = list(self.optimize_model.rr_parameters.keys).index(key) 552 self.mysmashmodel.rr_parameters.values[:, :, pos] = ( 553 self.optimize_model.rr_parameters.values[:, :, pos_w] 554 ) 555 # self.mysmashmodel.rr_parameters = self.optimize_model.rr_parameters 556 557 # needed here inorder to replace the rainfall without rebuild the model 558 self._gathering_atmosdata() 559 560 if invert_states: 561 if self._fstates is not None and not np.all(self._fstates == -99.0): 562 563 # TODO FIX BUG IN SMASH when states > 1.0 564 mask_inf = np.where(self.mysmashmodel.mesh.active_cell == 1) 565 mask_sup = np.where(self._fstates > 1) 566 self._fstates[mask_sup] = 0.9999 567 568 self.mysmashmodel.rr_initial_states.values[mask_inf] = self._fstates[ 569 mask_inf 570 ].copy() 571 572 else: 573 print( 574 "</> model final states does not exist." 575 " Invert states is not possible." 576 " Smash default initial states is used." 577 ) 578 579 self.extra_smash_results = self.mysmashmodel.forward_run( 580 cost_options=cost_options, 581 common_options=common_options, 582 return_options=return_options, 583 ) 584 585 # states backup 586 self._fstates = self.mysmashmodel.rr_final_states.values.copy() 587 self._istates = self.mysmashmodel.rr_initial_states.values.copy() 588 589 @tools.autocast_args 590 def atmos_data_connector( 591 self, 592 input_prcp: np.ndarray | None = None, 593 input_pet: np.ndarray | None = None, 594 input_dt: float | None = None, 595 input_res: tuple | list = (1000.0, 1000.0), 596 input_start_time: str = "2050-01-01 01:00", 597 input_bbox: dict | None = None, 598 input_epsg: int = 2154, 599 resampling_method="home_made_with_scipy_zoom", 600 ): 601 """ 602 Generate a connector for the external atmos_data comming from 603 other model such as Graffas (spatial rainfall generator). 604 605 Parameters 606 ---------- 607 608 input_prcp : np.ndarray | None = None 609 An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial rainfall 610 which will be used by Smash.Ideally, the extend of this array match 611 exactlly the extend of the Smash domain. 612 input_pet : np.ndarray | None = None 613 An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial 614 evapotranspiration which will be used by Smash. 615 input_dt : float = 3600. 616 Time step in seconds of the input precipitation 617 input_res : tuple | list = (1000., 1000.) 618 The resolution of the input precipitation 619 input_start_time : str = "2050-01-01 01:00" 620 The date of the start_time 621 input_bbox : dict | None = None 622 The extend of the domain using a bbox. 623 The convention used here is a dictionary like 624 bbox={"left":xmin, "top": ymax, "right": xmax, "bottom": ymin}. 625 If not provided, the extend of the smash domain will be used starting 626 from xmin and ymin. 627 input_epsg : int = 2154 628 The epsg code of the coordinate system. If not provided, 629 the coordinate system used in Smash will be used. 630 resampling_method: str 631 The method to use to resample and crop the input array. Default is 632 'home_made_with_scipy_zoom' Choice are: 633 ['rasterio_1', 'rasterio_2', 'home_made_with_scipy_zoom']. 634 'home_made_with_scipy_zoom' is the fastest method. 'rasterio_1' 635 is the slowest method. However, 'rasterio' method use much tested and 636 reliable method to resample and crop the array. 637 638 Examples 639 -------- 640 641 >>> prcp_array=np.zeros(200,200,20)+1. 642 >>> es=smashbox.SmashBox() 643 >>> sb.newmodel("RealCollobrier") 644 >>> sb.RealCollobrier.generate_mesh() 645 >>> sb.RealCollobrier.atmos_data_connector(input_prcp=prcp_array) 646 647 """ 648 if input_prcp is None and input_pet is None: 649 raise ValueError( 650 "</> input_prcp and input_pet are None. input_prcp or input_pet must be" 651 " numpy.ndarray with shape (nrow, ncol, ntimesteps)." 652 ) 653 654 if input_dt is None: 655 print( 656 "</> We suppose the input atmos data time-step equal to the model time-step." 657 ) 658 input_dt = self.mysetup.setup["dt"] 659 660 if input_dt != self.mysetup.setup["dt"]: 661 print( 662 "</> Model and input atmos data have different time-step. " 663 "Input atmos data will be resampled." 664 ) 665 if input_prcp is not None: 666 input_prcp = tools.time_resample_prcp_array( 667 input_prcp, 668 input_dt, 669 self.mysetup.setup["dt"], 670 t_axis=2, 671 ) 672 if input_pet is not None: 673 input_pet = tools.time_resample_prcp_array( 674 input_pet, 675 input_dt, 676 self.mysetup.setup["dt"], 677 t_axis=2, 678 ) 679 input_dt = self.mysetup.setup["dt"] 680 681 # use conversion factor defined in setup 682 if "prcp_conversion_factor" in self.mysetup.setup and input_prcp is not None: 683 input_prcp = input_prcp * self.mysetup.setup["prcp_conversion_factor"] 684 if "pet_conversion_factor" in self.mysetup.setup and input_pet is not None: 685 input_pet = input_pet * self.mysetup.setup["pet_conversion_factor"] 686 687 self._myatmos_data_connector = atmos_data_connector.atmos_data_connector( 688 input_prcp=input_prcp, 689 input_pet=input_pet, 690 input_dt=input_dt, 691 input_res=input_res, 692 input_start_time=input_start_time, 693 input_bbox=input_bbox, 694 input_epsg=input_epsg, 695 ) 696 697 smash_bbox = geo_toolbox.get_bbox_from_smash_mesh(self.mymesh.mesh) 698 699 self._myatmos_data_connector.read_input_atmos_data( 700 output_bbox=smash_bbox, 701 output_epsg=self._myparam.param.epsg, 702 output_res=(self.mymesh.mesh["xres"], self.mymesh.mesh["yres"]), 703 output_shape=(self.mymesh.mesh["nrow"], self.mymesh.mesh["ncol"]), 704 resampling_method=resampling_method, 705 ) 706 707 self._myatmos_data_connector.change_setup(self.mysetup) 708 709 def import_parameters(self, model=None): 710 """ 711 Import Geotiff parameter in Smash. This function wrap 712 smash.io.read_grid_parameters(). Path to the parameters is defined in 713 self._myparam.param.smash_parameters. 714 715 Parameter 716 --------- 717 718 model: smash.Model object 719 A Smash model object. If None, the Smash model of smashbox will be used 720 721 Examples 722 -------- 723 724 >>> es=smashbox.SmashBox() 725 >>> sb.newmodel("RealCollobrier") 726 >>> sb.RealCollobrier.generate_mesh(run=False) 727 >>> sb.RealCollobrier.model() 728 >>> sb.RealCollobrier.import_parameters() 729 >>> sb.RealCollobrier.forward_run() 730 """ 731 if self._myparam.param.smash_parameters is None: 732 print( 733 "</> Warning: no calibrated Smash parameters is used, leaving it to" 734 " default." 735 ) 736 return 737 738 if model is None: 739 model = self.mysmashmodel 740 741 smash.io.read_grid_parameters( 742 model=model, 743 path=self._myparam.param.smash_parameters, 744 ) 745 746 def export_parameters(self, path: os.PathLike = "./output_smash_param"): 747 """ 748 Export Geotiff Smash parameter as Geotiff. This function wrap 749 smash.io.save_grid_parameters(). 750 751 Parameters 752 ---------- 753 754 path : os.PathLike = "./output_smash_param" 755 path to a directory where the parameter will be saved. 756 757 Examples 758 -------- 759 760 >>> es=smashbox.SmashBox() 761 >>> sb.newmodel("RealCollobrier") 762 >>> sb.RealCollobrier.generate_mesh(run=False) 763 >>> sb.RealCollobrier.model() 764 >>> sb.RealCollobrier.import_parameters() 765 >>> sb.RealCollobrier.export_parameters() 766 """ 767 smash.io.export_parameters(self.mysmashmodel, path) 768 769 def save_model_container( 770 self, path_to_hdf5: str | None = None, save_smash_model: bool = True 771 ): 772 """ 773 774 :param path_to_hdf5: Path to the hdf5 file, defaults to None. If None, 775 the fucntion will return a dictionnary containing all data of the self object. 776 :type path_to_hdf5: str | None, optional 777 :param save_smash_model: Savec the smash models objects or no, defaults 778 to True. If False the Smash models objects are not saved. 779 :type save_smash_model: str, optional 780 :return: if a path_to_hdf5 is None, a dictionary containing all 781 attribute of the input object self is returned. 782 :rtype: dict | None 783 784 """ 785 786 if path_to_hdf5 is not None: 787 788 structure = pyhdf5_handler.src.object_handler.generate_object_structure( 789 self, include_method=False 790 ) 791 792 if not save_smash_model: 793 del structure["warmup_model"] 794 del structure["mysmashmodel"] 795 del structure["optimize_model"] 796 797 pyhdf5_handler.save_object_to_hdf5file( 798 path_to_hdf5=path_to_hdf5, 799 instance=self, 800 keys_data=structure, 801 location=f'./{self._model_name}/' 802 ) 803 804 else: 805 dict_results = pyhdf5_handler.src.object_handler.read_object_as_dict(self) 806 807 if not save_smash_model: 808 del dict_results["warmup_model"] 809 del dict_results["mysmashmodel"] 810 del structure["optimize_model"] 811 812 return dict_results 813 814 # def import_parameters(self) 815 # if os.path.exists(self._myparam.param.smash_parameters) and self._myparam.param.smash_parameters.endswith(".hdf5"): 816 817 # hdf5= pyhdf5_handler.open_hdf5(self._myparam.param.smash_parameters) 818 # hdf5_key=list(hdf5.keys()) 819 # hdf5.close() 820 821 # if "model_ddt" in hdf5_key: 822 # with_param = pyhdf5_handler.read_hdf5file_as_dict(self._myparam.param.smash_parameters,location="model_ddt/rr_parameters") 823 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model_ddt",item="mesh") 824 825 # smash_parameters.transfert_params_to_model( 826 # from_mesh= 827 # with_mesh, 828 # with_param=with_param, 829 # to_model=self.mysmashmodel) 830 831 # elif "model" in hdf5_key: 832 # with_param = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model",item="rr_parameters") 833 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model",item="mesh") 834 835 # smash_parameters.transfert_params_to_model( 836 # from_mesh= 837 # with_mesh, 838 # with_param=with_param, 839 # to_model=self.mysmashmodel) 840 841 # elif "rr_parameters" in hdf5.keys() and "mesh" in hdf5.keys(): 842 # with_param = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="./",item="rr_parameters") 843 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="./",item="mesh") 844 845 # smash_parameters.transfert_params_to_model( 846 # from_mesh=with_mesh, 847 # with_param=with_param, 848 # to_model=self.mysmashmodel) 849 850 # else: 851 # raise ValueError("</> Error: cannot load the parameters ... unknown format...") 852 853 # elif os.path.isdir(self._myparam.param.smash_parameters): 854 855 # smash_parameters.load_param_from_tiffformat(model=self.mysmashmodel, path_to_parameters=self._myparam.param.smash_parameters) 856 857 # else: 858 # raise ValueError(f"</> Error: cannot load the parameters. Path or file '{self._myparam.param.smash_parameters}' does not exist ... ") 859 860 # def write_smashparam(self, hdf5file : str | os.PathLike = "output_smash_param.hdf5"): 861 862 # rr_parameters=pyhdf5_handler.read_object_as_dict(self.mysmashmodel.rr_parameters) 863 # pyhdf5_handler.save_dict_to_hdf5(hdf5file, rr_parameters) 864 865 # def export_parameters(self, path : os.path = "./output_smash_param") 866 867 # list_param=list(self.mysmashmodel.rr_parameters.keys) 868 869 # for smparam in list_param: 870 871 # array=self.mysmashmodel.rr_parameters.values[:,:,list_param.index(smparam)] 872 873 # smash_parameters.write_array_to_geotiff(os.path.join(path, smparam+".tif"), 874 # array, 875 # self.mysmashmodel.mesh.xmin, 876 # self.mysmashmodel.mesh.ymax, 877 # output_res = (self.mymesh.mesh["xres"], 878 # self.mymesh.mesh["yres"]) 879 # )
25class model: 26 """Main class model() which has a complete set of functions, class and attributes to 27 build, run and manipulate the Smash model, compute statistical criterium and 28 plot graphics.""" 29 30 def __init__(self, name, myparam): 31 32 self._model_name = name 33 34 self._myparam = copy.deepcopy(myparam) 35 """The attribute _myparam is a copy of the parent class smashbox.myparam. 36 This class stores the main parameters used for building the Smash model""" 37 38 self.mysetup = setup.setup(self._myparam.param) 39 """The attribute mysetup own the class setup.setup(). This class stores the Smash 40 setup for the hydrological simulation and some helpers to manipulate these 41 parameters.""" 42 43 self.mymesh = mesh.mesh(self.mysetup) 44 """The attribute mymesh own the class mesh.mesh(). This class stores the Smash 45 mesh used for the hydrological simulation and some helpers to manipulate this mesh. 46 """ 47 48 self.mysmashmodel = None 49 """The attribute mysmashmodel stores the Smash model object created with 50 attributes mysetup and -mymesh.""" 51 52 self._fstates = None 53 self._istates = None 54 self._myatmos_data_connector = None 55 56 self.warmup_model = None 57 """The attribute warmup_model store a smash model used for warmup and compatible 58 with the model in attribute mysmashmodel""" 59 60 self.optimize_model = None 61 """The attribute optimize_model store a smash model used for optimization and compatible 62 with the model in attribute mysmashmodel""" 63 64 self.extra_smash_results = None 65 """The attribute extra_smash_results stores the extra results provided by Smash 66 if the parameter return_option is defined.""" 67 68 self.mystats = mystats.mystats(self) 69 """The attribute mystats own the class mystats.mystats(). This class stores 70 statistical functions and results which could be applied on the smash model.""" 71 72 self.myplot = myplot.myplot(self) 73 """The attribute myplot own the class myplot.myplot(). This class stores plotting 74 capabilities on the smash model object.""" 75 76 def generate_mesh( 77 self, 78 max_depth: float = 1.0, 79 query: str | None = None, 80 area_error_th: None | float = None, 81 lacuna_threshold: None | float = None, 82 ): 83 """ 84 Generate the mesh of the Smash model 85 86 Parameters 87 ---------- 88 89 max_depth : `int`, default 1 90 The maximum depth accepted by the algorithm to find the catchment outlet. 91 A **max_depth** of 1 means that the algorithm will search among the 92 combinations in 93 (``row - 1``, ``row``, ``row + 1``; ``col - 1``, ``col``, ``col + 1``), 94 the coordinates that minimize 95 the relative error between the given catchment area and the modeled 96 catchment area calculated from the 97 flow directions file. 98 :param query: Any pandas dataframe query as string: '(SURF>20) & (SURF<100)'. This query 99 must be build using the field (column name) in the outlet database. 100 https://pandas.pydata.org/docs/user_guide/indexing.html#the-query-method 101 :type query: str 102 area_error_th: float | None 103 The tolerance error for the difference between the observed and simulated 104 surface. The error is computed as follow: 105 Serror=abs(Ssim-Sobs)/Sobs 106 All outlets where `Serror > area_error_th` will be automatically removed from 107 the mesh. 108 :type area_error_th: float 109 :param lacuna_threshold: Lacuna threshold for the discharges in percent. All gauge where the proportion of lacuna exceed this threshold will be removed. 110 :type: float | Nonetype 111 112 Examples 113 -------- 114 115 >>> es=smashbox.SmashBox() 116 >>> sb.newmodel("RealCollobrier") 117 >>> sb.RealCollobrier.generate_mesh(min_surf=5, max_surf=100) 118 119 """ 120 self.mymesh.generate_mesh( 121 self._myparam.param, 122 max_depth=max_depth, 123 query=query, 124 area_error_th=area_error_th, 125 lacuna_threshold=lacuna_threshold, 126 ) 127 128 def model(self, setup: dict | None = None, mesh: dict | None = None, read_data=None): 129 """ 130 Smash model object creation. This function wrap smash.Model(). Setup and mesh 131 argument are optional since these dictionnary are hosted by the smashbox object. 132 133 Parameters 134 ---------- 135 setup: dict | None 136 The Smash setup (optionnal), if None the smashbox setup will be used 137 mesh: dict | None 138 The Smash mesh (optionnal), if None the smashbox mesh will be used 139 140 Examples 141 -------- 142 143 >>> es=smashbox.SmashBox() 144 >>> sb.newmodel("RealCollobrier") 145 >>> sb.RealCollobrier.generate_mesh(run=False) 146 >>> sb.RealCollobrier.model() 147 148 """ 149 if setup is None: 150 setup = self.mysetup.setup.copy() 151 152 if mesh is None: 153 mesh = self.mymesh.mesh.copy() 154 155 if read_data is False: 156 setup.update( 157 { 158 "read_prcp": False, 159 "read_pet": False, 160 "read_snow": False, 161 "read_qobs": False, 162 "read_temp": False, 163 } 164 ) 165 if read_data is True: 166 setup.update( 167 { 168 "read_prcp": True, 169 "read_pet": True, 170 "read_snow": True, 171 "read_qobs": True, 172 "read_temp": True, 173 } 174 ) 175 176 self.mysmashmodel = self._model(setup=setup, mesh=mesh) 177 178 if read_data is True: 179 self._gathering_atmosdata() 180 181 self._gathering_parameters(self.mysmashmodel) 182 183 def _model(self, setup=None, mesh=None): 184 """ 185 Smash model object creation. This function wrap smash.Model() 186 187 Parameters 188 ---------- 189 setup: dict | None 190 The Smash setup (optionnal), if None the smashbox setup will be used 191 mesh: dict | None 192 The Smash mesh (optionnal), if None the smashbox mesh will be used 193 194 Examples 195 -------- 196 197 >>> es=smashbox.SmashBox() 198 >>> sb.newmodel("RealCollobrier") 199 >>> sb.RealCollobrier.generate_mesh(run=False) 200 >>> sb.RealCollobrier.model() 201 202 """ 203 if setup is None: 204 print("</> The input setup is None ") 205 return None 206 207 if mesh is None: 208 print("</> input mesh is None. use smashbox.model.model.generate_mesh()") 209 return None 210 211 if self._myparam.param.enhanced_smash_input_data: 212 model = smashmodel.SmashModel(setup, mesh) 213 else: 214 model = smash.Model(setup, mesh) 215 216 return model 217 218 def _gathering_parameters(self, model): 219 220 if self.optimize_model is not None: 221 print(f"</> Getting parameters from previously optimized model ...") 222 model.rr_parameters = self.optimize_model.rr_parameters 223 else: 224 print( 225 f"</> Importing parameters from {self._myparam.param._smash_parameters} ..." 226 ) 227 self.import_parameters(model=model) 228 self._transform_parameters( 229 model=model, dt_origin=self._myparam.param._smash_parameters_dt 230 ) 231 232 def _transform_parameters(self, model=None, dt_origin: None | float = None): 233 """ 234 Function to transform the parameters according the model time-step and the original 235 time-step used for calibrated the parameters. 236 :param dt_origin: Original time-step used for generate the calibrated parameter, 237 defaults to None 238 :type dt_origin: None | float, optional 239 240 """ 241 # if dt_origin is None: 242 # raise ValueError( 243 # " Argument dt_origin is None. This must be filled with the value " 244 # "of the timestep used to calibrate the parameters." 245 # ) 246 247 # if not hasattr(self, "mysmashmodel") or self.mysmashmodel is None: 248 # raise ValueError( 249 # "</> mysmashmodel attribute does not exist or is None. " 250 # "The model must be created first..." 251 # ) 252 if model is None: 253 return 254 255 dt_target = model.setup.dt 256 257 if dt_origin is None: 258 return 259 260 if dt_target == dt_origin: 261 return 262 263 print( 264 f"</> Tranforming parameters `ct`, `kexec`, `llr` calibrated with a time-step " 265 f"of {dt_origin}s to the modeled time-step {dt_target}s." 266 ) 267 parameters_tranfsorm = ["ct", "kexc", "llr"] 268 power_tranform = [1.0 / 4.0, -1.0 / 8.0, 1.0] 269 270 for i, param in enumerate(parameters_tranfsorm): 271 index_param = np.where(param == model.rr_parameters.keys)[0] 272 if len(index_param) > 0: 273 model.rr_parameters.values[:, :, index_param[0]] = ( 274 model.rr_parameters.values[:, :, index_param[0]] 275 * (dt_origin / dt_target) ** power_tranform[i] 276 ) 277 278 def _gathering_atmosdata(self): 279 280 if self._myatmos_data_connector is not None: 281 282 print("</> Gathering atmos data ...") 283 284 if ( 285 self._myatmos_data_connector.input_ntimestep 286 != self.mysmashmodel.setup.ntime_step 287 ): 288 print( 289 "</> Warnings: Inconsistant ntime_step " 290 f"{self._myatmos_data_connector.input_ntimestep}" 291 f"!={self.mysmashmodel.setup.ntime_step}, " 292 "the Smash model will be rebuild." 293 ) 294 295 self._model() 296 self._gathering_parameters(self.mysmashmodel) 297 298 if self._myatmos_data_connector.smash_prcp is not None: 299 self.mysmashmodel.atmos_data.prcp = ( 300 self._myatmos_data_connector.smash_prcp 301 ) 302 303 if self._myatmos_data_connector.smash_pet is not None: 304 self.mysmashmodel.atmos_data.pet = self._myatmos_data_connector.smash_pet 305 306 if self.mysmashmodel.setup.prcp_partitioning: 307 print("</> Compute prcp partitionning ...") 308 smash.fcore._mw_atmos_statistic.compute_prcp_partitioning( 309 model.setup, model.mesh, model._input_data 310 ) 311 312 if self.mysmashmodel.setup.compute_mean_atmos: 313 print("</> Computing mean atmospheric data") 314 smash.fcore._mw_atmos_statistic.compute_mean_atmos( 315 self.mysmashmodel.setup, 316 self.mysmashmodel.mesh, 317 self.mysmashmodel._input_data, 318 ) 319 320 @tools.autocast_args 321 def model_warmup(self, warmup: int = 365): 322 """ 323 Smash model warmup function. This function warm the curent model by creating a new 324 on with attribute warmup_model. The final states of warmup_model are copied to the 325 initial states of mysmashmodel. 326 327 Parameters 328 ---------- 329 330 warmup: None | int 331 a integer of the number of days used for warming the model. 332 333 """ 334 print("</> Warmup the Smash-model...") 335 336 if warmup is not None: 337 try: 338 timedelta = datetime.timedelta(days=warmup) 339 except: 340 raise ValueError(f"warmup arg `{warmup}` is not a valid time delta.") 341 342 w_setup = copy.deepcopy(self.mysetup.setup) 343 344 w_setup["end_time"] = w_setup["start_time"] 345 w_setup["start_time"] = datetime.datetime.strftime( 346 datetime.datetime.fromisoformat(w_setup["start_time"]) - timedelta, 347 "%Y-%m-%d %H:%M", 348 ) 349 w_setup["read_qobs"] = False 350 w_setup["read_prcp"] = True 351 w_setup["read_pet"] = True 352 353 if self.warmup_model is None: 354 self.warmup_model = self._model(setup=w_setup, mesh=self.mymesh.mesh) 355 356 self._gathering_parameters(self.warmup_model) 357 self.warmup_model.forward_run() 358 359 # TODO FIX BUG IN SMASH when states > 1.0 360 mask_sup = np.where(self.warmup_model.rr_final_states.values > 1) 361 self.warmup_model.rr_final_states.values[mask_sup] = 0.9999 362 363 if hasattr(self, "mysmashmodel") and self.mysmashmodel is not None: 364 365 self.mysmashmodel.rr_initial_states = self.warmup_model.rr_final_states 366 367 def optimize( 368 self, 369 start_time: None | str = None, 370 end_time: None | str = None, 371 mapping: str = "uniform", 372 optimizer: None | str = None, 373 optimize_options: None | str = None, 374 cost_options: None | str = None, 375 common_options: None | str = None, 376 return_options: None | str = None, 377 callback=None, 378 ): 379 """ 380 Optimize the current model (with the current setup and mesh), 381 store the model in the attribute optimize_model and set the calibrated 382 parameters to the model behind the attribute mysmashmodel. 383 Start_time and end_time can be specified here to change the period of the 384 calibration compare to the current setup. All other arguments are 385 equivalent to the smash.model.optimize function 386 (see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize). 387 One difference from Smash is that gauges with no data are 388 automatically removed from the optimization without raising an error. 389 This provide a convient way to calibrate quickly the parameters using the current 390 mesh which may include gauges with data for calibration 391 and location gauge for discharges computation. 392 393 :param start_time: The start time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 394 :type start_time: None | str, optional 395 :param end_time: The end time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 396 :type end_time: None | str, optional 397 :param mapping: Type of mapping, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to "uniform" 398 :type mapping: str, optional 399 :param optimizer: Name of optimizer, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 400 :type optimizer: None | str, optional 401 :param optimize_options: Dictionary containing optimization options for fine-tuning the optimization process, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 402 :type optimize_options: None | str, optional 403 :param cost_options: Dictionary containing computation cost options for simulated and observed responses. see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 404 :type cost_options: None | str, optional 405 :param common_options: Dictionary containing common options with two elements: ncpu (int, default 1) and verbose (bool, default is False), defaults to None 406 :type common_options: None | str, optional 407 :param return_options: Dictionary containing return options to save additional simulation results, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 408 :type return_options: None | str, optional 409 :param callback: A callable called after each iteration with the signature callback(iopt: Optimize), see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 410 :type callback: TYPE, optional 411 412 """ 413 o_setup = copy.deepcopy(self.mysetup.setup) 414 415 if start_time is not None and end_time is not None: 416 o_setup["start_time"] = start_time 417 o_setup["end_time"] = end_time 418 419 o_setup["read_qobs"] = True 420 o_setup["read_prcp"] = True 421 o_setup["read_pet"] = True 422 423 if self.optimize_model is None: 424 self.optimize_model = self._model(setup=o_setup, mesh=self.mymesh.mesh) 425 print(f"</> Getting parameters from the default model `mysmashmodel` ...") 426 427 for key in list(self.optimize_model.rr_parameters.keys): 428 pos = list(self.optimize_model.rr_parameters.keys).index(key) 429 pos_w = list(self.mysmashmodel.rr_parameters.keys).index(key) 430 self.optimize_model.rr_parameters.values[:, :, pos] = ( 431 self.mysmashmodel.rr_parameters.values[:, :, pos_w] 432 ) 433 # ~ self.optimize_model.rr_parameters = self.mysmashmodel.rr_parameters.copy() 434 435 if self.warmup_model is not None: 436 for key in list(self.optimize_model.rr_initial_states.keys): 437 pos = list(self.optimize_model.rr_initial_states.keys).index(key) 438 pos_w = list(self.warmup_model.rr_initial_states.keys).index(key) 439 self.optimize_model.rr_initial_states.values[:, :, pos] = ( 440 self.warmup_model.rr_final_states.values[:, :, pos_w] 441 ) 442 443 default_cost_option = {"end_warmup": o_setup["start_time"], "gauge": "all"} 444 if cost_options is not None: 445 default_cost_option.update(cost_options) 446 447 # auto remove gauge if no observation 448 if isinstance(default_cost_option["gauge"], str): 449 if default_cost_option["gauge"] == "dws": 450 gauge = np.empty(shape=0) 451 452 for i, pos in enumerate(self.optimize_model.mesh.gauge_pos): 453 if self.optimize_model.mesh.flwdst[tuple(pos)] == 0: 454 gauge = np.append(gauge, self.optimize_model.mesh.code[i]) 455 456 elif default_cost_option["gauge"] == "all": 457 gauge = np.array(self.optimize_model.mesh.code, ndmin=1) 458 else: 459 gauge = np.array(default_cost_option["gauge"], ndmin=1) 460 elif isinstance(default_cost_option["gauge"], list): 461 gauge = np.array(default_cost_option["gauge"], ndmin=1) 462 463 st = pd.Timestamp(self.optimize_model.setup.start_time) 464 et = pd.Timestamp(self.optimize_model.setup.end_time) 465 ew = pd.Timestamp(default_cost_option["end_warmup"]) 466 start_slice = int((ew - st).total_seconds() / self.optimize_model.setup.dt) 467 end_slice = int((et - ew).total_seconds() / self.optimize_model.setup.dt) 468 time_slice = slice(start_slice, end_slice) 469 470 del_gauge = [] 471 for i in range(len(gauge)): 472 473 if np.all(self.optimize_model.response_data.q[i, time_slice] < 0): 474 del_gauge.append(i) 475 476 print( 477 f"No observed discharge available at gauge '{gauge[i]}' for the selected " 478 f"optimization period ['{ew}', '{et}']. This gauge is removed " 479 f"from the optimization." 480 ) 481 482 gauge = np.delete(gauge, del_gauge) 483 default_cost_option.update({"gauge": gauge}) 484 485 self.optimize_model.optimize( 486 mapping, 487 optimizer, 488 optimize_options, 489 default_cost_option, 490 common_options, 491 return_options, 492 callback, 493 ) 494 495 if hasattr(self, "mysmashmodel") and self.mysmashmodel is not None: 496 self.mysmashmodel.rr_parameters = self.optimize_model.rr_parameters.copy() 497 498 @tools.autocast_args 499 def forward_run( 500 self, 501 warmup: int | None = None, 502 invert_states: bool | None = False, 503 cost_options: dict | None = None, 504 common_options: dict | None = None, 505 return_options: dict | None = None, 506 ): 507 """ 508 Smash model forward run.This function wrap smash.model.forward_run(). 509 510 Parameters 511 ---------- 512 513 warmup: None | int 514 a integer of the number of days used for warming the model. 515 invert_states : bool = False 516 invert states of the model, so that the final states are used 517 for the initial states. 518 cost_options : dict | None, 519 Dictionary containing computation cost options for simulated and observed responses (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html). 520 common_options: dict| None, 521 Dictionary containing common options with two elements, ncpu and verbose (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html) 522 return_options : dict | None, 523 Dictionary containing return options to save additional simulation results. (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html) 524 525 Examples 526 -------- 527 528 >>> es=smashbox.SmashBox() 529 >>> sb.newmodel("RealCollobrier") 530 >>> sb.RealCollobrier.generate_mesh(run=False) 531 >>> sb.RealCollobrier.model() 532 >>> sb.RealCollobrier.forward_run() 533 """ 534 535 if self.mysmashmodel is None: 536 self.model() 537 538 if warmup is not None: 539 self.model_warmup(warmup=warmup) 540 541 if self.warmup_model is not None: 542 for key in list(self.mysmashmodel.rr_initial_states.keys): 543 pos = list(self.mysmashmodel.rr_initial_states.keys).index(key) 544 pos_w = list(self.warmup_model.rr_initial_states.keys).index(key) 545 self.mysmashmodel.rr_initial_states.values[:, :, pos] = ( 546 self.warmup_model.rr_final_states.values[:, :, pos_w] 547 ) 548 549 if self.optimize_model is not None: 550 for key in list(self.mysmashmodel.rr_parameters.keys): 551 pos = list(self.mysmashmodel.rr_parameters.keys).index(key) 552 pos_w = list(self.optimize_model.rr_parameters.keys).index(key) 553 self.mysmashmodel.rr_parameters.values[:, :, pos] = ( 554 self.optimize_model.rr_parameters.values[:, :, pos_w] 555 ) 556 # self.mysmashmodel.rr_parameters = self.optimize_model.rr_parameters 557 558 # needed here inorder to replace the rainfall without rebuild the model 559 self._gathering_atmosdata() 560 561 if invert_states: 562 if self._fstates is not None and not np.all(self._fstates == -99.0): 563 564 # TODO FIX BUG IN SMASH when states > 1.0 565 mask_inf = np.where(self.mysmashmodel.mesh.active_cell == 1) 566 mask_sup = np.where(self._fstates > 1) 567 self._fstates[mask_sup] = 0.9999 568 569 self.mysmashmodel.rr_initial_states.values[mask_inf] = self._fstates[ 570 mask_inf 571 ].copy() 572 573 else: 574 print( 575 "</> model final states does not exist." 576 " Invert states is not possible." 577 " Smash default initial states is used." 578 ) 579 580 self.extra_smash_results = self.mysmashmodel.forward_run( 581 cost_options=cost_options, 582 common_options=common_options, 583 return_options=return_options, 584 ) 585 586 # states backup 587 self._fstates = self.mysmashmodel.rr_final_states.values.copy() 588 self._istates = self.mysmashmodel.rr_initial_states.values.copy() 589 590 @tools.autocast_args 591 def atmos_data_connector( 592 self, 593 input_prcp: np.ndarray | None = None, 594 input_pet: np.ndarray | None = None, 595 input_dt: float | None = None, 596 input_res: tuple | list = (1000.0, 1000.0), 597 input_start_time: str = "2050-01-01 01:00", 598 input_bbox: dict | None = None, 599 input_epsg: int = 2154, 600 resampling_method="home_made_with_scipy_zoom", 601 ): 602 """ 603 Generate a connector for the external atmos_data comming from 604 other model such as Graffas (spatial rainfall generator). 605 606 Parameters 607 ---------- 608 609 input_prcp : np.ndarray | None = None 610 An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial rainfall 611 which will be used by Smash.Ideally, the extend of this array match 612 exactlly the extend of the Smash domain. 613 input_pet : np.ndarray | None = None 614 An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial 615 evapotranspiration which will be used by Smash. 616 input_dt : float = 3600. 617 Time step in seconds of the input precipitation 618 input_res : tuple | list = (1000., 1000.) 619 The resolution of the input precipitation 620 input_start_time : str = "2050-01-01 01:00" 621 The date of the start_time 622 input_bbox : dict | None = None 623 The extend of the domain using a bbox. 624 The convention used here is a dictionary like 625 bbox={"left":xmin, "top": ymax, "right": xmax, "bottom": ymin}. 626 If not provided, the extend of the smash domain will be used starting 627 from xmin and ymin. 628 input_epsg : int = 2154 629 The epsg code of the coordinate system. If not provided, 630 the coordinate system used in Smash will be used. 631 resampling_method: str 632 The method to use to resample and crop the input array. Default is 633 'home_made_with_scipy_zoom' Choice are: 634 ['rasterio_1', 'rasterio_2', 'home_made_with_scipy_zoom']. 635 'home_made_with_scipy_zoom' is the fastest method. 'rasterio_1' 636 is the slowest method. However, 'rasterio' method use much tested and 637 reliable method to resample and crop the array. 638 639 Examples 640 -------- 641 642 >>> prcp_array=np.zeros(200,200,20)+1. 643 >>> es=smashbox.SmashBox() 644 >>> sb.newmodel("RealCollobrier") 645 >>> sb.RealCollobrier.generate_mesh() 646 >>> sb.RealCollobrier.atmos_data_connector(input_prcp=prcp_array) 647 648 """ 649 if input_prcp is None and input_pet is None: 650 raise ValueError( 651 "</> input_prcp and input_pet are None. input_prcp or input_pet must be" 652 " numpy.ndarray with shape (nrow, ncol, ntimesteps)." 653 ) 654 655 if input_dt is None: 656 print( 657 "</> We suppose the input atmos data time-step equal to the model time-step." 658 ) 659 input_dt = self.mysetup.setup["dt"] 660 661 if input_dt != self.mysetup.setup["dt"]: 662 print( 663 "</> Model and input atmos data have different time-step. " 664 "Input atmos data will be resampled." 665 ) 666 if input_prcp is not None: 667 input_prcp = tools.time_resample_prcp_array( 668 input_prcp, 669 input_dt, 670 self.mysetup.setup["dt"], 671 t_axis=2, 672 ) 673 if input_pet is not None: 674 input_pet = tools.time_resample_prcp_array( 675 input_pet, 676 input_dt, 677 self.mysetup.setup["dt"], 678 t_axis=2, 679 ) 680 input_dt = self.mysetup.setup["dt"] 681 682 # use conversion factor defined in setup 683 if "prcp_conversion_factor" in self.mysetup.setup and input_prcp is not None: 684 input_prcp = input_prcp * self.mysetup.setup["prcp_conversion_factor"] 685 if "pet_conversion_factor" in self.mysetup.setup and input_pet is not None: 686 input_pet = input_pet * self.mysetup.setup["pet_conversion_factor"] 687 688 self._myatmos_data_connector = atmos_data_connector.atmos_data_connector( 689 input_prcp=input_prcp, 690 input_pet=input_pet, 691 input_dt=input_dt, 692 input_res=input_res, 693 input_start_time=input_start_time, 694 input_bbox=input_bbox, 695 input_epsg=input_epsg, 696 ) 697 698 smash_bbox = geo_toolbox.get_bbox_from_smash_mesh(self.mymesh.mesh) 699 700 self._myatmos_data_connector.read_input_atmos_data( 701 output_bbox=smash_bbox, 702 output_epsg=self._myparam.param.epsg, 703 output_res=(self.mymesh.mesh["xres"], self.mymesh.mesh["yres"]), 704 output_shape=(self.mymesh.mesh["nrow"], self.mymesh.mesh["ncol"]), 705 resampling_method=resampling_method, 706 ) 707 708 self._myatmos_data_connector.change_setup(self.mysetup) 709 710 def import_parameters(self, model=None): 711 """ 712 Import Geotiff parameter in Smash. This function wrap 713 smash.io.read_grid_parameters(). Path to the parameters is defined in 714 self._myparam.param.smash_parameters. 715 716 Parameter 717 --------- 718 719 model: smash.Model object 720 A Smash model object. If None, the Smash model of smashbox will be used 721 722 Examples 723 -------- 724 725 >>> es=smashbox.SmashBox() 726 >>> sb.newmodel("RealCollobrier") 727 >>> sb.RealCollobrier.generate_mesh(run=False) 728 >>> sb.RealCollobrier.model() 729 >>> sb.RealCollobrier.import_parameters() 730 >>> sb.RealCollobrier.forward_run() 731 """ 732 if self._myparam.param.smash_parameters is None: 733 print( 734 "</> Warning: no calibrated Smash parameters is used, leaving it to" 735 " default." 736 ) 737 return 738 739 if model is None: 740 model = self.mysmashmodel 741 742 smash.io.read_grid_parameters( 743 model=model, 744 path=self._myparam.param.smash_parameters, 745 ) 746 747 def export_parameters(self, path: os.PathLike = "./output_smash_param"): 748 """ 749 Export Geotiff Smash parameter as Geotiff. This function wrap 750 smash.io.save_grid_parameters(). 751 752 Parameters 753 ---------- 754 755 path : os.PathLike = "./output_smash_param" 756 path to a directory where the parameter will be saved. 757 758 Examples 759 -------- 760 761 >>> es=smashbox.SmashBox() 762 >>> sb.newmodel("RealCollobrier") 763 >>> sb.RealCollobrier.generate_mesh(run=False) 764 >>> sb.RealCollobrier.model() 765 >>> sb.RealCollobrier.import_parameters() 766 >>> sb.RealCollobrier.export_parameters() 767 """ 768 smash.io.export_parameters(self.mysmashmodel, path) 769 770 def save_model_container( 771 self, path_to_hdf5: str | None = None, save_smash_model: bool = True 772 ): 773 """ 774 775 :param path_to_hdf5: Path to the hdf5 file, defaults to None. If None, 776 the fucntion will return a dictionnary containing all data of the self object. 777 :type path_to_hdf5: str | None, optional 778 :param save_smash_model: Savec the smash models objects or no, defaults 779 to True. If False the Smash models objects are not saved. 780 :type save_smash_model: str, optional 781 :return: if a path_to_hdf5 is None, a dictionary containing all 782 attribute of the input object self is returned. 783 :rtype: dict | None 784 785 """ 786 787 if path_to_hdf5 is not None: 788 789 structure = pyhdf5_handler.src.object_handler.generate_object_structure( 790 self, include_method=False 791 ) 792 793 if not save_smash_model: 794 del structure["warmup_model"] 795 del structure["mysmashmodel"] 796 del structure["optimize_model"] 797 798 pyhdf5_handler.save_object_to_hdf5file( 799 path_to_hdf5=path_to_hdf5, 800 instance=self, 801 keys_data=structure, 802 location=f'./{self._model_name}/' 803 ) 804 805 else: 806 dict_results = pyhdf5_handler.src.object_handler.read_object_as_dict(self) 807 808 if not save_smash_model: 809 del dict_results["warmup_model"] 810 del dict_results["mysmashmodel"] 811 del structure["optimize_model"] 812 813 return dict_results 814 815 # def import_parameters(self) 816 # if os.path.exists(self._myparam.param.smash_parameters) and self._myparam.param.smash_parameters.endswith(".hdf5"): 817 818 # hdf5= pyhdf5_handler.open_hdf5(self._myparam.param.smash_parameters) 819 # hdf5_key=list(hdf5.keys()) 820 # hdf5.close() 821 822 # if "model_ddt" in hdf5_key: 823 # with_param = pyhdf5_handler.read_hdf5file_as_dict(self._myparam.param.smash_parameters,location="model_ddt/rr_parameters") 824 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model_ddt",item="mesh") 825 826 # smash_parameters.transfert_params_to_model( 827 # from_mesh= 828 # with_mesh, 829 # with_param=with_param, 830 # to_model=self.mysmashmodel) 831 832 # elif "model" in hdf5_key: 833 # with_param = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model",item="rr_parameters") 834 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="model",item="mesh") 835 836 # smash_parameters.transfert_params_to_model( 837 # from_mesh= 838 # with_mesh, 839 # with_param=with_param, 840 # to_model=self.mysmashmodel) 841 842 # elif "rr_parameters" in hdf5.keys() and "mesh" in hdf5.keys(): 843 # with_param = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="./",item="rr_parameters") 844 # with_mesh = pyhdf5_handler.get_hdf5file_item(self._myparam.param.smash_parameters,location="./",item="mesh") 845 846 # smash_parameters.transfert_params_to_model( 847 # from_mesh=with_mesh, 848 # with_param=with_param, 849 # to_model=self.mysmashmodel) 850 851 # else: 852 # raise ValueError("</> Error: cannot load the parameters ... unknown format...") 853 854 # elif os.path.isdir(self._myparam.param.smash_parameters): 855 856 # smash_parameters.load_param_from_tiffformat(model=self.mysmashmodel, path_to_parameters=self._myparam.param.smash_parameters) 857 858 # else: 859 # raise ValueError(f"</> Error: cannot load the parameters. Path or file '{self._myparam.param.smash_parameters}' does not exist ... ") 860 861 # def write_smashparam(self, hdf5file : str | os.PathLike = "output_smash_param.hdf5"): 862 863 # rr_parameters=pyhdf5_handler.read_object_as_dict(self.mysmashmodel.rr_parameters) 864 # pyhdf5_handler.save_dict_to_hdf5(hdf5file, rr_parameters) 865 866 # def export_parameters(self, path : os.path = "./output_smash_param") 867 868 # list_param=list(self.mysmashmodel.rr_parameters.keys) 869 870 # for smparam in list_param: 871 872 # array=self.mysmashmodel.rr_parameters.values[:,:,list_param.index(smparam)] 873 874 # smash_parameters.write_array_to_geotiff(os.path.join(path, smparam+".tif"), 875 # array, 876 # self.mysmashmodel.mesh.xmin, 877 # self.mysmashmodel.mesh.ymax, 878 # output_res = (self.mymesh.mesh["xres"], 879 # self.mymesh.mesh["yres"]) 880 # )
Main class model() which has a complete set of functions, class and attributes to build, run and manipulate the Smash model, compute statistical criterium and plot graphics.
30 def __init__(self, name, myparam): 31 32 self._model_name = name 33 34 self._myparam = copy.deepcopy(myparam) 35 """The attribute _myparam is a copy of the parent class smashbox.myparam. 36 This class stores the main parameters used for building the Smash model""" 37 38 self.mysetup = setup.setup(self._myparam.param) 39 """The attribute mysetup own the class setup.setup(). This class stores the Smash 40 setup for the hydrological simulation and some helpers to manipulate these 41 parameters.""" 42 43 self.mymesh = mesh.mesh(self.mysetup) 44 """The attribute mymesh own the class mesh.mesh(). This class stores the Smash 45 mesh used for the hydrological simulation and some helpers to manipulate this mesh. 46 """ 47 48 self.mysmashmodel = None 49 """The attribute mysmashmodel stores the Smash model object created with 50 attributes mysetup and -mymesh.""" 51 52 self._fstates = None 53 self._istates = None 54 self._myatmos_data_connector = None 55 56 self.warmup_model = None 57 """The attribute warmup_model store a smash model used for warmup and compatible 58 with the model in attribute mysmashmodel""" 59 60 self.optimize_model = None 61 """The attribute optimize_model store a smash model used for optimization and compatible 62 with the model in attribute mysmashmodel""" 63 64 self.extra_smash_results = None 65 """The attribute extra_smash_results stores the extra results provided by Smash 66 if the parameter return_option is defined.""" 67 68 self.mystats = mystats.mystats(self) 69 """The attribute mystats own the class mystats.mystats(). This class stores 70 statistical functions and results which could be applied on the smash model.""" 71 72 self.myplot = myplot.myplot(self) 73 """The attribute myplot own the class myplot.myplot(). This class stores plotting 74 capabilities on the smash model object."""
The attribute mysetup own the class setup.setup(). This class stores the Smash setup for the hydrological simulation and some helpers to manipulate these parameters.
The attribute mymesh own the class mesh.mesh(). This class stores the Smash mesh used for the hydrological simulation and some helpers to manipulate this mesh.
The attribute mysmashmodel stores the Smash model object created with attributes mysetup and -mymesh.
The attribute warmup_model store a smash model used for warmup and compatible with the model in attribute mysmashmodel
The attribute optimize_model store a smash model used for optimization and compatible with the model in attribute mysmashmodel
The attribute extra_smash_results stores the extra results provided by Smash if the parameter return_option is defined.
The attribute mystats own the class mystats.mystats(). This class stores statistical functions and results which could be applied on the smash model.
The attribute myplot own the class myplot.myplot(). This class stores plotting capabilities on the smash model object.
76 def generate_mesh( 77 self, 78 max_depth: float = 1.0, 79 query: str | None = None, 80 area_error_th: None | float = None, 81 lacuna_threshold: None | float = None, 82 ): 83 """ 84 Generate the mesh of the Smash model 85 86 Parameters 87 ---------- 88 89 max_depth : `int`, default 1 90 The maximum depth accepted by the algorithm to find the catchment outlet. 91 A **max_depth** of 1 means that the algorithm will search among the 92 combinations in 93 (``row - 1``, ``row``, ``row + 1``; ``col - 1``, ``col``, ``col + 1``), 94 the coordinates that minimize 95 the relative error between the given catchment area and the modeled 96 catchment area calculated from the 97 flow directions file. 98 :param query: Any pandas dataframe query as string: '(SURF>20) & (SURF<100)'. This query 99 must be build using the field (column name) in the outlet database. 100 https://pandas.pydata.org/docs/user_guide/indexing.html#the-query-method 101 :type query: str 102 area_error_th: float | None 103 The tolerance error for the difference between the observed and simulated 104 surface. The error is computed as follow: 105 Serror=abs(Ssim-Sobs)/Sobs 106 All outlets where `Serror > area_error_th` will be automatically removed from 107 the mesh. 108 :type area_error_th: float 109 :param lacuna_threshold: Lacuna threshold for the discharges in percent. All gauge where the proportion of lacuna exceed this threshold will be removed. 110 :type: float | Nonetype 111 112 Examples 113 -------- 114 115 >>> es=smashbox.SmashBox() 116 >>> sb.newmodel("RealCollobrier") 117 >>> sb.RealCollobrier.generate_mesh(min_surf=5, max_surf=100) 118 119 """ 120 self.mymesh.generate_mesh( 121 self._myparam.param, 122 max_depth=max_depth, 123 query=query, 124 area_error_th=area_error_th, 125 lacuna_threshold=lacuna_threshold, 126 )
Generate the mesh of the Smash model
Parameters
max_depth : int, default 1
The maximum depth accepted by the algorithm to find the catchment outlet.
A max_depth of 1 means that the algorithm will search among the
combinations in
(row - 1, row, row + 1; col - 1, col, col + 1),
the coordinates that minimize
the relative error between the given catchment area and the modeled
catchment area calculated from the
flow directions file.
Parameters
- query: Any pandas dataframe query as string: '(SURF>20) & (SURF<100)'. This query
must be build using the field (column name) in the outlet database.
https://pandas.pydata.org/docs/user_guide/indexing.html#the-query-method
area_error_th: float | None
The tolerance error for the difference between the observed and simulated
surface. The error is computed as follow:
Serror=abs(Ssim-Sobs)/Sobs
All outlets where
Serror > area_error_thwill be automatically removed from the mesh. - lacuna_threshold: Lacuna threshold for the discharges in percent. All gauge where the proportion of lacuna exceed this threshold will be removed.
Examples
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh(min_surf=5, max_surf=100)
128 def model(self, setup: dict | None = None, mesh: dict | None = None, read_data=None): 129 """ 130 Smash model object creation. This function wrap smash.Model(). Setup and mesh 131 argument are optional since these dictionnary are hosted by the smashbox object. 132 133 Parameters 134 ---------- 135 setup: dict | None 136 The Smash setup (optionnal), if None the smashbox setup will be used 137 mesh: dict | None 138 The Smash mesh (optionnal), if None the smashbox mesh will be used 139 140 Examples 141 -------- 142 143 >>> es=smashbox.SmashBox() 144 >>> sb.newmodel("RealCollobrier") 145 >>> sb.RealCollobrier.generate_mesh(run=False) 146 >>> sb.RealCollobrier.model() 147 148 """ 149 if setup is None: 150 setup = self.mysetup.setup.copy() 151 152 if mesh is None: 153 mesh = self.mymesh.mesh.copy() 154 155 if read_data is False: 156 setup.update( 157 { 158 "read_prcp": False, 159 "read_pet": False, 160 "read_snow": False, 161 "read_qobs": False, 162 "read_temp": False, 163 } 164 ) 165 if read_data is True: 166 setup.update( 167 { 168 "read_prcp": True, 169 "read_pet": True, 170 "read_snow": True, 171 "read_qobs": True, 172 "read_temp": True, 173 } 174 ) 175 176 self.mysmashmodel = self._model(setup=setup, mesh=mesh) 177 178 if read_data is True: 179 self._gathering_atmosdata() 180 181 self._gathering_parameters(self.mysmashmodel)
Smash model object creation. This function wrap smash.Model(). Setup and mesh argument are optional since these dictionnary are hosted by the smashbox object.
Parameters
setup: dict | None The Smash setup (optionnal), if None the smashbox setup will be used mesh: dict | None The Smash mesh (optionnal), if None the smashbox mesh will be used
Examples
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh(run=False)
>>> sb.RealCollobrier.model()
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)
Smash model warmup function. This function warm the curent model by creating a new on with attribute warmup_model. The final states of warmup_model are copied to the initial states of mysmashmodel.
Parameters
warmup: None | int a integer of the number of days used for warming the model.
367 def optimize( 368 self, 369 start_time: None | str = None, 370 end_time: None | str = None, 371 mapping: str = "uniform", 372 optimizer: None | str = None, 373 optimize_options: None | str = None, 374 cost_options: None | str = None, 375 common_options: None | str = None, 376 return_options: None | str = None, 377 callback=None, 378 ): 379 """ 380 Optimize the current model (with the current setup and mesh), 381 store the model in the attribute optimize_model and set the calibrated 382 parameters to the model behind the attribute mysmashmodel. 383 Start_time and end_time can be specified here to change the period of the 384 calibration compare to the current setup. All other arguments are 385 equivalent to the smash.model.optimize function 386 (see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize). 387 One difference from Smash is that gauges with no data are 388 automatically removed from the optimization without raising an error. 389 This provide a convient way to calibrate quickly the parameters using the current 390 mesh which may include gauges with data for calibration 391 and location gauge for discharges computation. 392 393 :param start_time: The start time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 394 :type start_time: None | str, optional 395 :param end_time: The end time of the optimization, format "YYYY-mm-dd HH:MM", defaults to None 396 :type end_time: None | str, optional 397 :param mapping: Type of mapping, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to "uniform" 398 :type mapping: str, optional 399 :param optimizer: Name of optimizer, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 400 :type optimizer: None | str, optional 401 :param optimize_options: Dictionary containing optimization options for fine-tuning the optimization process, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 402 :type optimize_options: None | str, optional 403 :param cost_options: Dictionary containing computation cost options for simulated and observed responses. see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 404 :type cost_options: None | str, optional 405 :param common_options: Dictionary containing common options with two elements: ncpu (int, default 1) and verbose (bool, default is False), defaults to None 406 :type common_options: None | str, optional 407 :param return_options: Dictionary containing return options to save additional simulation results, see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 408 :type return_options: None | str, optional 409 :param callback: A callable called after each iteration with the signature callback(iopt: Optimize), see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None 410 :type callback: TYPE, optional 411 412 """ 413 o_setup = copy.deepcopy(self.mysetup.setup) 414 415 if start_time is not None and end_time is not None: 416 o_setup["start_time"] = start_time 417 o_setup["end_time"] = end_time 418 419 o_setup["read_qobs"] = True 420 o_setup["read_prcp"] = True 421 o_setup["read_pet"] = True 422 423 if self.optimize_model is None: 424 self.optimize_model = self._model(setup=o_setup, mesh=self.mymesh.mesh) 425 print(f"</> Getting parameters from the default model `mysmashmodel` ...") 426 427 for key in list(self.optimize_model.rr_parameters.keys): 428 pos = list(self.optimize_model.rr_parameters.keys).index(key) 429 pos_w = list(self.mysmashmodel.rr_parameters.keys).index(key) 430 self.optimize_model.rr_parameters.values[:, :, pos] = ( 431 self.mysmashmodel.rr_parameters.values[:, :, pos_w] 432 ) 433 # ~ self.optimize_model.rr_parameters = self.mysmashmodel.rr_parameters.copy() 434 435 if self.warmup_model is not None: 436 for key in list(self.optimize_model.rr_initial_states.keys): 437 pos = list(self.optimize_model.rr_initial_states.keys).index(key) 438 pos_w = list(self.warmup_model.rr_initial_states.keys).index(key) 439 self.optimize_model.rr_initial_states.values[:, :, pos] = ( 440 self.warmup_model.rr_final_states.values[:, :, pos_w] 441 ) 442 443 default_cost_option = {"end_warmup": o_setup["start_time"], "gauge": "all"} 444 if cost_options is not None: 445 default_cost_option.update(cost_options) 446 447 # auto remove gauge if no observation 448 if isinstance(default_cost_option["gauge"], str): 449 if default_cost_option["gauge"] == "dws": 450 gauge = np.empty(shape=0) 451 452 for i, pos in enumerate(self.optimize_model.mesh.gauge_pos): 453 if self.optimize_model.mesh.flwdst[tuple(pos)] == 0: 454 gauge = np.append(gauge, self.optimize_model.mesh.code[i]) 455 456 elif default_cost_option["gauge"] == "all": 457 gauge = np.array(self.optimize_model.mesh.code, ndmin=1) 458 else: 459 gauge = np.array(default_cost_option["gauge"], ndmin=1) 460 elif isinstance(default_cost_option["gauge"], list): 461 gauge = np.array(default_cost_option["gauge"], ndmin=1) 462 463 st = pd.Timestamp(self.optimize_model.setup.start_time) 464 et = pd.Timestamp(self.optimize_model.setup.end_time) 465 ew = pd.Timestamp(default_cost_option["end_warmup"]) 466 start_slice = int((ew - st).total_seconds() / self.optimize_model.setup.dt) 467 end_slice = int((et - ew).total_seconds() / self.optimize_model.setup.dt) 468 time_slice = slice(start_slice, end_slice) 469 470 del_gauge = [] 471 for i in range(len(gauge)): 472 473 if np.all(self.optimize_model.response_data.q[i, time_slice] < 0): 474 del_gauge.append(i) 475 476 print( 477 f"No observed discharge available at gauge '{gauge[i]}' for the selected " 478 f"optimization period ['{ew}', '{et}']. This gauge is removed " 479 f"from the optimization." 480 ) 481 482 gauge = np.delete(gauge, del_gauge) 483 default_cost_option.update({"gauge": gauge}) 484 485 self.optimize_model.optimize( 486 mapping, 487 optimizer, 488 optimize_options, 489 default_cost_option, 490 common_options, 491 return_options, 492 callback, 493 ) 494 495 if hasattr(self, "mysmashmodel") and self.mysmashmodel is not None: 496 self.mysmashmodel.rr_parameters = self.optimize_model.rr_parameters.copy()
Optimize the current model (with the current setup and mesh), store the model in the attribute optimize_model and set the calibrated parameters to the model behind the attribute mysmashmodel. Start_time and end_time can be specified here to change the period of the calibration compare to the current setup. All other arguments are equivalent to the smash.model.optimize function (see https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize). One difference from Smash is that gauges with no data are automatically removed from the optimization without raising an error. This provide a convient way to calibrate quickly the parameters using the current mesh which may include gauges with data for calibration and location gauge for discharges computation.
Parameters
- start_time: The start time of the optimization, format "YYYY-mm-dd HH: MM", defaults to None
- end_time: The end time of the optimization, format "YYYY-mm-dd HH: MM", defaults to None
- mapping: Type of mapping, see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to "uniform"
- optimizer: Name of optimizer, see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None
- optimize_options: Dictionary containing optimization options for fine-tuning the optimization process, see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None
- cost_options: Dictionary containing computation cost options for simulated and observed responses. see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None
- common_options: Dictionary containing common options with two elements: ncpu (int, default 1) and verbose (bool, default is False), defaults to None
- return_options: Dictionary containing return options to save additional simulation results, see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None
- callback: A callable called after each iteration with the signature callback(iopt: Optimize), see https: //smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.optimize.html#smash.optimize, defaults to None
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)
Smash model forward run.This function wrap smash.model.forward_run().
Parameters
warmup: None | int a integer of the number of days used for warming the model. invert_states : bool = False invert states of the model, so that the final states are used for the initial states. cost_options : dict | None, Dictionary containing computation cost options for simulated and observed responses (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html). common_options: dict| None, Dictionary containing common options with two elements, ncpu and verbose (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html) return_options : dict | None, Dictionary containing return options to save additional simulation results. (https://smash.recover.inrae.fr/api_reference/principal_methods/smash/smash.Model.forward_run.html)
Examples
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh(run=False)
>>> sb.RealCollobrier.model()
>>> sb.RealCollobrier.forward_run()
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)
Generate a connector for the external atmos_data comming from other model such as Graffas (spatial rainfall generator).
Parameters
input_prcp : np.ndarray | None = None An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial rainfall which will be used by Smash.Ideally, the extend of this array match exactlly the extend of the Smash domain. input_pet : np.ndarray | None = None An np.ndarray with shape (nrow, ncol, npdt) wich stores the spatial evapotranspiration which will be used by Smash. input_dt : float = 3600. Time step in seconds of the input precipitation input_res : tuple | list = (1000., 1000.) The resolution of the input precipitation input_start_time : str = "2050-01-01 01:00" The date of the start_time input_bbox : dict | None = None The extend of the domain using a bbox. The convention used here is a dictionary like bbox={"left":xmin, "top": ymax, "right": xmax, "bottom": ymin}. If not provided, the extend of the smash domain will be used starting from xmin and ymin. input_epsg : int = 2154 The epsg code of the coordinate system. If not provided, the coordinate system used in Smash will be used. resampling_method: str The method to use to resample and crop the input array. Default is 'home_made_with_scipy_zoom' Choice are: ['rasterio_1', 'rasterio_2', 'home_made_with_scipy_zoom']. 'home_made_with_scipy_zoom' is the fastest method. 'rasterio_1' is the slowest method. However, 'rasterio' method use much tested and reliable method to resample and crop the array.
Examples
>>> prcp_array=np.zeros(200,200,20)+1.
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh()
>>> sb.RealCollobrier.atmos_data_connector(input_prcp=prcp_array)
710 def import_parameters(self, model=None): 711 """ 712 Import Geotiff parameter in Smash. This function wrap 713 smash.io.read_grid_parameters(). Path to the parameters is defined in 714 self._myparam.param.smash_parameters. 715 716 Parameter 717 --------- 718 719 model: smash.Model object 720 A Smash model object. If None, the Smash model of smashbox will be used 721 722 Examples 723 -------- 724 725 >>> es=smashbox.SmashBox() 726 >>> sb.newmodel("RealCollobrier") 727 >>> sb.RealCollobrier.generate_mesh(run=False) 728 >>> sb.RealCollobrier.model() 729 >>> sb.RealCollobrier.import_parameters() 730 >>> sb.RealCollobrier.forward_run() 731 """ 732 if self._myparam.param.smash_parameters is None: 733 print( 734 "</> Warning: no calibrated Smash parameters is used, leaving it to" 735 " default." 736 ) 737 return 738 739 if model is None: 740 model = self.mysmashmodel 741 742 smash.io.read_grid_parameters( 743 model=model, 744 path=self._myparam.param.smash_parameters, 745 )
Import Geotiff parameter in Smash. This function wrap smash.io.read_grid_parameters(). Path to the parameters is defined in self._myparam.param.smash_parameters.
Parameter
model: smash.Model object A Smash model object. If None, the Smash model of smashbox will be used
Examples
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh(run=False)
>>> sb.RealCollobrier.model()
>>> sb.RealCollobrier.import_parameters()
>>> sb.RealCollobrier.forward_run()
747 def export_parameters(self, path: os.PathLike = "./output_smash_param"): 748 """ 749 Export Geotiff Smash parameter as Geotiff. This function wrap 750 smash.io.save_grid_parameters(). 751 752 Parameters 753 ---------- 754 755 path : os.PathLike = "./output_smash_param" 756 path to a directory where the parameter will be saved. 757 758 Examples 759 -------- 760 761 >>> es=smashbox.SmashBox() 762 >>> sb.newmodel("RealCollobrier") 763 >>> sb.RealCollobrier.generate_mesh(run=False) 764 >>> sb.RealCollobrier.model() 765 >>> sb.RealCollobrier.import_parameters() 766 >>> sb.RealCollobrier.export_parameters() 767 """ 768 smash.io.export_parameters(self.mysmashmodel, path)
Export Geotiff Smash parameter as Geotiff. This function wrap smash.io.save_grid_parameters().
Parameters
path : os.PathLike = "./output_smash_param" path to a directory where the parameter will be saved.
Examples
>>> es=smashbox.SmashBox()
>>> sb.newmodel("RealCollobrier")
>>> sb.RealCollobrier.generate_mesh(run=False)
>>> sb.RealCollobrier.model()
>>> sb.RealCollobrier.import_parameters()
>>> sb.RealCollobrier.export_parameters()
770 def save_model_container( 771 self, path_to_hdf5: str | None = None, save_smash_model: bool = True 772 ): 773 """ 774 775 :param path_to_hdf5: Path to the hdf5 file, defaults to None. If None, 776 the fucntion will return a dictionnary containing all data of the self object. 777 :type path_to_hdf5: str | None, optional 778 :param save_smash_model: Savec the smash models objects or no, defaults 779 to True. If False the Smash models objects are not saved. 780 :type save_smash_model: str, optional 781 :return: if a path_to_hdf5 is None, a dictionary containing all 782 attribute of the input object self is returned. 783 :rtype: dict | None 784 785 """ 786 787 if path_to_hdf5 is not None: 788 789 structure = pyhdf5_handler.src.object_handler.generate_object_structure( 790 self, include_method=False 791 ) 792 793 if not save_smash_model: 794 del structure["warmup_model"] 795 del structure["mysmashmodel"] 796 del structure["optimize_model"] 797 798 pyhdf5_handler.save_object_to_hdf5file( 799 path_to_hdf5=path_to_hdf5, 800 instance=self, 801 keys_data=structure, 802 location=f'./{self._model_name}/' 803 ) 804 805 else: 806 dict_results = pyhdf5_handler.src.object_handler.read_object_as_dict(self) 807 808 if not save_smash_model: 809 del dict_results["warmup_model"] 810 del dict_results["mysmashmodel"] 811 del structure["optimize_model"] 812 813 return dict_results
Parameters
- path_to_hdf5: Path to the hdf5 file, defaults to None. If None, the fucntion will return a dictionnary containing all data of the self object.
- save_smash_model: Savec the smash models objects or no, defaults to True. If False the Smash models objects are not saved.
Returns
if a path_to_hdf5 is None, a dictionary containing all attribute of the input object self is returned.