smashbox 1.0__py2.py3-none-any.whl

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