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    #                            )
class model:
 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.

model(name, myparam)
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."""
mysetup

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.

mymesh

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.

mysmashmodel

The attribute mysmashmodel stores the Smash model object created with attributes mysetup and -mymesh.

warmup_model

The attribute warmup_model store a smash model used for warmup and compatible with the model in attribute mysmashmodel

optimize_model

The attribute optimize_model store a smash model used for optimization and compatible with the model in attribute mysmashmodel

extra_smash_results

The attribute extra_smash_results stores the extra results provided by Smash if the parameter return_option is defined.

mystats

The attribute mystats own the class mystats.mystats(). This class stores statistical functions and results which could be applied on the smash model.

myplot

The attribute myplot own the class myplot.myplot(). This class stores plotting capabilities on the smash model object.

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

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.

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

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

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)
def import_parameters(self, model=None):
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()
def export_parameters(self, path: os.PathLike = './output_smash_param'):
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()
def save_model_container(self, path_to_hdf5: str | None = None, save_smash_model: bool = True):
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.