wolfhece 2.1.127__py3-none-any.whl → 2.1.129__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.
@@ -16,6 +16,7 @@ from ..PyTranslate import _
16
16
 
17
17
  from scipy.spatial import KDTree
18
18
  import pandas as pd
19
+ from datetime import datetime
19
20
  from pathlib import Path
20
21
  import numpy as np
21
22
  from typing import Literal, Union
@@ -27,22 +28,32 @@ from matplotlib.axes import Axes
27
28
 
28
29
 
29
30
  class InjectionType(Enum):
30
- GLOBAL = 'Global'
31
- PARTIAL = 'Partial'
32
- ANTHROPOGENIC = 'Anthropogenic'
33
- CONSTANT = 'Constant'
34
- VIRTUAL = 'Virtual'
31
+ GLOBAL = 'Global' # Global hydrograph - sum of upstream watershed and local hydrograph (hydrological model)
32
+ PARTIAL = 'Partial' # Partial hydrograph - local hydrograph only (hydrological model)
33
+ ANTHROPOGENIC = 'Anthropogenic' # Output of an anthropogenic module (hydrological model)
34
+ CONSTANT = 'Constant' # Constant value (user input)
35
+ VIRTUAL = 'Virtual' # Virtual hydrograph - Combination/Part of hydrographs (computed by the coupling)
36
+ FORCED_UNSTEADY = 'Forced unsteady' # Imposed unsteady flow (user input)
35
37
 
38
+ BUILDING_TOLERANCE = 0.05 # Epsilon for the buildings (DEM-DTM) [m]
36
39
 
37
40
  class Searching_Context():
41
+ """ Part of the hydrological model adapted for seraching tasks """
38
42
 
39
- def __init__(self,
40
- river_axis:vector,
41
- kdtree:KDTree,
42
- nodes:Node_Watershed,
43
- downstream_reaches:list[int],
43
+ def __init__(self,
44
+ river_axis:vector,
45
+ kdtree:KDTree,
46
+ nodes:Node_Watershed,
47
+ downstream_reaches:list[int],
44
48
  up_node:Node_Watershed) -> None:
45
-
49
+ """
50
+ :param river_axis: river axis -- vector
51
+ :param kdtree: KDTree of the downstream reaches
52
+ :param nodes: nodes of the downstream reaches - from Hydrology model
53
+ :param downstream_reaches: downstream reaches - from Hydrology model
54
+ :param up_node: up node - from Hydrology model
55
+ """
56
+
46
57
  self.river_axis:vector = river_axis # river axis -- vector
47
58
  self.kdtree:KDTree = kdtree # KDTree of the downstream reaches
48
59
  self.nodes:Node_Watershed = nodes # nodes of the downstream reaches - from Hydrology model
@@ -50,7 +61,8 @@ class Searching_Context():
50
61
  self.up_node:Node_Watershed = up_node # up node - from Hydrology model
51
62
 
52
63
  def __str__(self) -> str:
53
-
64
+ """ Return a string representation of the searching context """
65
+
54
66
  ret = ''
55
67
 
56
68
  ret += f" Number of reaches: {len(self.downstream_reaches)}\n"
@@ -64,7 +76,15 @@ class Searching_Context():
64
76
  class Scaled_Infiltration():
65
77
 
66
78
  def __init__(self, idx:int, type:InjectionType, colref:str, factor:float, lagtime:float) -> None:
67
-
79
+ """ Constructor of the scaled infiltration
80
+
81
+ :param idx: index of the infiltration
82
+ :param type: type of the infiltration
83
+ :param colref: reference column
84
+ :param factor: multiplicator factor [-]
85
+ :param lagtime: lag time [s]
86
+ """
87
+
68
88
  self.index = idx # index of the infiltration
69
89
  self.type = type # type of the infiltration
70
90
  self.colref = colref # reference column
@@ -74,7 +94,7 @@ class Scaled_Infiltration():
74
94
  class Coupling_Hydrology_2D():
75
95
 
76
96
  def __init__(self) -> None:
77
-
97
+
78
98
 
79
99
  self._rivers_zones:list[Zones] = []
80
100
  self.rivers:dict[str, vector] = {}
@@ -92,7 +112,10 @@ class Coupling_Hydrology_2D():
92
112
  self.hydrographs_local:pd.DataFrame = None
93
113
  self._hydrographs_virtual = []
94
114
 
95
- self._dem:WolfArray = None
115
+ self._dem:WolfArray = None # Digital Elevation Model
116
+ self._dtm:WolfArray = None # Digital Terrain Model -- Optional -- if exists, no infiltration authorized on buildings (defined as dem > dtm)
117
+ self._buildings:WolfArray = None # Buildings -- exists if dtm exists
118
+
96
119
  self._infil_idx:WolfArray = None
97
120
  self.counter_zone_infil = 0
98
121
 
@@ -100,7 +123,7 @@ class Coupling_Hydrology_2D():
100
123
  # Chaque zone est définie par :
101
124
  # - un type d'infiltration
102
125
  # - un nom d'hydrogramme (ou une valeur constante)
103
- # - un facteur podérateur
126
+ # - un facteur podérateur
104
127
  # - un temps de déphasage
105
128
 
106
129
  self.infiltrations:list[Scaled_Infiltration] = []
@@ -120,11 +143,25 @@ class Coupling_Hydrology_2D():
120
143
  self_along = None
121
144
  self._locales = None
122
145
 
146
+ @property
147
+ def dateBegin(self) -> datetime:
148
+ if self._hydrology_model is None:
149
+ logging.error(_("No hydrology model loaded"))
150
+ return None
151
+ return self._hydrology_model.dateBegin
152
+
153
+ @property
154
+ def dateEnd(self) -> datetime:
155
+ if self._hydrology_model is None:
156
+ logging.error(_("No hydrology model loaded"))
157
+ return None
158
+ return self._hydrology_model.dateEnd
159
+
123
160
  def __str__(self) -> str:
124
161
  """ Return a string representation of the coupling """
125
-
162
+
126
163
  ret =''
127
-
164
+
128
165
  ret += _("Rivers: {}\n").format(len(self.rivers))
129
166
  for curriver in self.rivers:
130
167
  ret += f"{curriver}\n"
@@ -145,22 +182,22 @@ class Coupling_Hydrology_2D():
145
182
  ret += f"{cursearch} : \n{self._searching[cursearch]}\n"
146
183
 
147
184
  ret += f"Coupling array: \n{self._infil_idx}\n"
148
-
185
+
149
186
  return ret
150
187
 
151
188
  @property
152
189
  def number_of_injections(self) -> int:
153
-
190
+
154
191
  if self._infil_idx is None:
155
192
  logging.error(_("No infiltration array -- Spread the injections first"))
156
193
  return 0
157
194
 
158
195
  return self._infil_idx.array.max()
159
-
196
+
160
197
  @property
161
198
  def number_of_nodes_per_zone(self) -> dict[int, int]:
162
199
  """ Return the number of nodes per zone """
163
-
200
+
164
201
  if self._infil_idx is None:
165
202
  logging.error(_("No infiltration array -- Spread the injections first"))
166
203
  return {}
@@ -168,7 +205,7 @@ class Coupling_Hydrology_2D():
168
205
  non_zeros = self._infil_idx.array[np.where(self._infil_idx.array > 0)]
169
206
 
170
207
  return {i:np.count_nonzero(non_zeros == i) for i in range(1, self.number_of_injections+1)}
171
-
208
+
172
209
  def plot_number_of_nodes_per_zone(self) -> tuple[Figure, Axes]:
173
210
  """ Plot the number of nodes per zone """
174
211
 
@@ -183,20 +220,20 @@ class Coupling_Hydrology_2D():
183
220
  ax.set_ylabel(_("Number of nodes"))
184
221
 
185
222
  return fig, ax
186
-
223
+
187
224
  @property
188
225
  def along(self) -> list[str, str]:
189
226
  return self._along
190
-
227
+
191
228
  @along.setter
192
229
  def along(self, value:list[tuple[str, str]]) -> None:
193
230
 
194
231
  for curval in value:
195
-
232
+
196
233
  curcol, currivers = curval
197
-
234
+
198
235
  assert curcol in self.hydrographs_local.columns, f"Column {curcol} not found in hydrographs"
199
-
236
+
200
237
  def check_rivers(currivers):
201
238
  if isinstance(currivers, list):
202
239
  for curriver in currivers:
@@ -209,36 +246,46 @@ class Coupling_Hydrology_2D():
209
246
  @property
210
247
  def locales(self) -> list[str]:
211
248
  return self._locales
212
-
249
+
213
250
  @locales.setter
214
- def locales(self, value:list[str]) -> None:
251
+ def locales(self, value:list[tuple[str, str | pd.Series | float, InjectionType]]) -> None:
252
+ """ Set the locales injections """
215
253
 
216
254
  for curvect, curcol, curtype in value:
255
+ # Check if the data is correct
217
256
 
218
- if curtype != InjectionType.CONSTANT:
257
+ if curtype not in [InjectionType.CONSTANT, InjectionType.FORCED_UNSTEADY]:
219
258
  assert curvect in self.locale_injections, f"Vector {curvect} not found"
220
259
  assert curcol in list(self.hydrographs_total.columns)+ self._hydrographs_virtual, f"Column {curcol} not found in hydrographs"
260
+ elif curtype == InjectionType.FORCED_UNSTEADY:
261
+ assert isinstance(curcol, pd.Series), "Forced unsteady flow should be a Series"
262
+ # check if the DateFrame has value inside interval of the hydrographs
263
+ assert curcol.index[0] <= self.dateBegin, "Forced unsteady flow should start before the hydrographs"
264
+ assert curcol.index[-1] >= self.dateEnd, "Forced unsteady flow should end after the hydrographs"
265
+ else:
266
+ assert isinstance(curcol, float), "Constant value should be a float"
221
267
 
222
268
  self._locales = value
223
269
 
224
270
  @property
225
271
  def watershed(self) -> Watershed:
226
272
  return self._hydrology_model.charact_watrshd
227
-
273
+
228
274
  @property
229
275
  def river_system(self) -> RiverSystem:
230
276
  return self.watershed.riversystem
231
-
277
+
232
278
  @property
233
279
  def subs_array(self) -> WolfArray:
234
280
  return self.watershed.subs_array
235
-
236
- def set_array_to_coupling(self, array:WolfArray | Path) -> None:
237
- """ Set the array to coupling
238
-
281
+
282
+ def set_array_to_coupling(self, array:WolfArray | Path, dtm:WolfArray | Path = None) -> None:
283
+ """ Set the array to coupling
284
+
239
285
  :param array: The array to coupling
286
+ :param dtm: The DTM of the array
240
287
  """
241
-
288
+
242
289
  if isinstance(array, Path):
243
290
  if array.exists():
244
291
  self._dem = WolfArray(array)
@@ -246,7 +293,18 @@ class Coupling_Hydrology_2D():
246
293
  logging.error(_("File {} not found").format(array))
247
294
  return
248
295
 
296
+ if isinstance(dtm, Path):
297
+ if dtm.exists():
298
+ self._dtm = WolfArray(dtm)
299
+ else:
300
+ logging.error(_("File {} not found").format(dtm))
301
+ return
302
+
249
303
  self._dem = array
304
+ self._dtm = dtm
305
+
306
+ assert self._dem.get_header().is_like(self._dtm.get_header()), "DEM and DTM should have the same header"
307
+
250
308
  self._create_infiltration_array()
251
309
 
252
310
  def _create_infiltration_array(self):
@@ -256,34 +314,51 @@ class Coupling_Hydrology_2D():
256
314
  logging.error(_("No array to coupling"))
257
315
  return
258
316
 
317
+ if self._dtm is None:
318
+ logging.info(_("No DTM found -- Infiltration authorized everywhere"))
319
+ else:
320
+ logging.info(_("DTM found -- Infiltration authorized only on the ground"))
321
+ self._buildings = self._dem - self._dtm
322
+
323
+ # Buildings are defined as the difference between the DEM and the DTM
324
+ # If the difference is greater than BUILDING_TOLERANCE, the cell is considered as a building
325
+ # Otherwise, the cell is considered as a ground
326
+
327
+ # If cell is masked in the DTM, cells is considred as ground
328
+ self._buildings.array[np.logical_and(~self._dem.array.mask,self._dtm.array.mask)] = 0.
329
+
330
+ self._buildings.array[self._buildings.array < BUILDING_TOLERANCE] = 0.
331
+ self._buildings.array[self._buildings.array >= BUILDING_TOLERANCE] = 1.
332
+ self._buildings.mask_data(0.)
333
+
259
334
  self._infil_idx = WolfArray(srcheader=self._dem.get_header(), whichtype=WOLF_ARRAY_FULL_INTEGER)
260
335
  self._infil_idx.add_ops_sel()
261
336
  self._infil_idx.array[:,:] = 0
262
337
 
263
- self.counter_zone_infil = 0
264
-
338
+ self.counter_zone_infil = 0
339
+
265
340
  def add_hydrology_model(self, name:str, filename:str | Path) -> None:
266
- """ Add a hydrology model to the coupling
267
-
341
+ """ Add a hydrology model to the coupling
342
+
268
343
  :param filename: The filename of the hydrology model
269
344
  """
270
-
345
+
271
346
  self._hydrology_model = Catchment(name, str(filename), False, True)
272
-
347
+
273
348
  def get_anthropogenic_names(self) -> list[str]:
274
349
  """ Print the names of the anthropogenic hydrographs """
275
-
350
+
276
351
  if self._hydrology_model is None:
277
352
  logging.error(_("No hydrology model loaded"))
278
353
  return []
279
354
 
280
- return [" : ".join([cur_anth.name, name])
281
- for cur_anth in self._hydrology_model.retentionBasinDict.values()
355
+ return [" : ".join([cur_anth.name, name])
356
+ for cur_anth in self._hydrology_model.retentionBasinDict.values()
282
357
  for name in cur_anth.get_outFlow_names()]
283
-
358
+
284
359
  def get_names_areas(self) -> list[str]:
285
360
  """ Print the names of the areas """
286
-
361
+
287
362
  if self._hydrology_model is None:
288
363
  logging.error(_("No hydrology model loaded"))
289
364
  return []
@@ -295,12 +370,12 @@ class Coupling_Hydrology_2D():
295
370
  return names, area_subs, area_glob
296
371
 
297
372
  def create_hydrographs_local_global(self, unit_discharge:float, total_duration:float, anth_discharge:dict = None) -> None:
298
- """
373
+ """
299
374
  Create the hydrographs from the hydrology model .
300
-
375
+
301
376
  Global and local hydrographs are created based on a unit discharge and a total duration.
302
377
 
303
- You can also add anthropogenic hydrographs from a dictionary.
378
+ You can also add anthropogenic hydrographs from a dictionary.
304
379
  The key is the name of the anthropogenic hydrograph and the value is the discharge.
305
380
  The keys can be obtained with the method get_anthropogenic_names.
306
381
 
@@ -311,14 +386,14 @@ class Coupling_Hydrology_2D():
311
386
  # Extract the column names according to their sorted subbasin indices
312
387
  col_time = "Time [s]"
313
388
  col_subs, area_subs, area_glob = self.get_names_areas()
314
- col_anth = [" : ".join([cur_anth.name, name])
315
- for cur_anth in self._hydrology_model.retentionBasinDict.values()
389
+ col_anth = [" : ".join([cur_anth.name, name])
390
+ for cur_anth in self._hydrology_model.retentionBasinDict.values()
316
391
  for name in cur_anth.get_outFlow_names()]
317
-
392
+
318
393
  #Create a dictionnary
319
394
 
320
395
  dict_glob = {col_time : [0., total_duration]}
321
-
396
+
322
397
  for cur_sub, cur_area in zip(col_subs, area_glob):
323
398
  discharge = cur_area * unit_discharge
324
399
  dict_glob[cur_sub] = [discharge, discharge]
@@ -350,23 +425,23 @@ class Coupling_Hydrology_2D():
350
425
  if self._hydrology_model is None:
351
426
  logging.error(_("No hydrology model loaded"))
352
427
  return
353
-
428
+
354
429
  directory = Path(self._hydrology_model.workingDir) / 'PostProcess'
355
430
 
356
431
  if total is not None:
357
432
  self.hydrographs_total.to_csv(directory / total, sep='\t', decimal='.', encoding='latin1')
358
433
  else:
359
434
  self.hydrographs_total.to_csv(directory / 'Hydros_2_simul2D.txt', sep='\t', decimal='.', encoding='latin1')
360
-
435
+
361
436
  if partial is not None:
362
437
  self.hydrographs_local.to_csv(directory / partial, sep='\t', decimal='.', encoding='latin1')
363
438
  else:
364
439
  self.hydrographs_local.to_csv(directory / 'HydrosSub_2_simul2D.txt', sep='\t', decimal='.', encoding='latin1')
365
440
 
366
-
441
+
367
442
  def load_hydrographs(self, directory:str | Path = None, total:str = None, partial:str = None) -> None:
368
- """ Load the hydrographs from the hydrology model
369
-
443
+ """ Load the hydrographs from the hydrology model
444
+
370
445
  :param directory: The directory of the hydrology model -- If None, the working directory of the loaded hydrology model is used
371
446
  :param total: The filename of the total hydrographs - If None, the default filename is used
372
447
  :param partial: The filename of the partial hydrographs - If None, the default filename is used
@@ -378,7 +453,7 @@ class Coupling_Hydrology_2D():
378
453
  if self._hydrology_model is None:
379
454
  logging.error(_("No hydrology model loaded"))
380
455
  return
381
-
456
+
382
457
  directory = Path(self._hydrology_model.workingDir) / 'PostProcess'
383
458
 
384
459
  if total is not None:
@@ -401,7 +476,7 @@ class Coupling_Hydrology_2D():
401
476
  self.hydrographs_local = pd.read_csv(partial, sep='\t', decimal='.', header=0, index_col=0, encoding='latin1')
402
477
  else:
403
478
  logging.error(_("File {} not found").format(partial))
404
-
479
+
405
480
  else:
406
481
  partial = directory / 'HydrosSub_2_simul2D.txt'
407
482
  if partial.exists():
@@ -409,18 +484,18 @@ class Coupling_Hydrology_2D():
409
484
  else:
410
485
  logging.error(_("File {} not found").format(partial))
411
486
 
412
-
487
+
413
488
  def print_hydrographs(self, total:bool = True, partial:bool = True) -> None:
414
489
  """ Print the hydrographs from the hydrology model """
415
490
 
416
491
  if total:
417
492
  print(_("Total hydrographs:"))
418
493
  print(self.hydrographs_total.columns)
419
-
494
+
420
495
  if partial:
421
496
  print(_("Partial hydrographs:"))
422
497
  print(self.hydrographs_local.columns)
423
-
498
+
424
499
  def plot_hydrographs(self, total:bool = True, partial:bool = True) -> tuple[tuple[Figure, Axes],tuple[Figure, Axes]]:
425
500
  """ Plot the hydrographs from the hydrology model """
426
501
 
@@ -430,9 +505,9 @@ class Coupling_Hydrology_2D():
430
505
  ax1.legend(loc='upper center', ncol=8)
431
506
  ax1.set_ylim(0, 1000)
432
507
  fig1.set_size_inches(15, 5)
433
- fig1.tight_layout()
508
+ fig1.tight_layout()
434
509
  else:
435
- fig1, ax1 = None, None
510
+ fig1, ax1 = None, None
436
511
 
437
512
  if partial:
438
513
  ax2 = self.hydrographs_local.plot()
@@ -440,11 +515,11 @@ class Coupling_Hydrology_2D():
440
515
  ax2.legend(loc='upper center', ncol=8)
441
516
  ax2.set_ylim(0, 1000)
442
517
  fig2.set_size_inches(15, 5)
443
- fig2.tight_layout()
518
+ fig2.tight_layout()
444
519
  else:
445
- fig2, ax2 = None, None
520
+ fig2, ax2 = None, None
446
521
 
447
- return (fig1, ax1), (fig2, ax2)
522
+ return (fig1, ax1), (fig2, ax2)
448
523
 
449
524
 
450
525
  def add_virtual_hydrograph(self, name:str, src_hydrograph_name:str, factor:float, lag:float=0.):
@@ -453,16 +528,16 @@ class Coupling_Hydrology_2D():
453
528
  self._hydrographs_virtual.append((name, src_hydrograph_name, factor, lag))
454
529
 
455
530
  def add_river(self, filename:str | Path) -> None:
456
- """ Add a river to the hydrology model
457
-
531
+ """ Add a river to the hydrology model
532
+
458
533
  :param filename: The filename of the river
459
534
  """
460
-
535
+
461
536
  self._rivers_zones.append(Zones(filename))
462
537
 
463
538
  def reset(self) -> None:
464
539
  """ Reset the hydrology model """
465
-
540
+
466
541
  self._rivers_zones = []
467
542
  self.rivers = {}
468
543
  self._locale_injection_zones = None
@@ -475,11 +550,11 @@ class Coupling_Hydrology_2D():
475
550
  self.reset_injections()
476
551
 
477
552
  def add_locale_injections(self, filename:str | Path) -> None:
478
- """ Add a local injection to the hydrology model
479
-
553
+ """ Add a local injection to the hydrology model
554
+
480
555
  :param filename: The filename of the local injection
481
556
  """
482
-
557
+
483
558
  self._locale_injection_zones = Zones(filename)
484
559
 
485
560
  def find_river_axis(self):
@@ -498,19 +573,19 @@ class Coupling_Hydrology_2D():
498
573
  logging.warning(_("Vector {} in zone {} is not used -- Ignoring it as a river axis").format(curvector.myname, curzone.myname))
499
574
 
500
575
  def _add_injection(self, name:str, vect:vector):
501
- """ Add an injection to the hydrology model
502
-
576
+ """ Add an injection to the hydrology model
577
+
503
578
  :param name: The name of the injection
504
579
  :param vect: The vector of the injection
505
580
  """
506
-
581
+
507
582
  self.locale_injections[name] = vect
508
583
 
509
584
  def find_injections(self):
510
585
  """ Find the injection points from Zones """
511
586
 
512
587
  for curzone in self._locale_injection_zones.myzones:
513
-
588
+
514
589
  names = [curvect.myname for curvect in curzone.myvectors]
515
590
 
516
591
  if 'injection' in names:
@@ -530,8 +605,8 @@ class Coupling_Hydrology_2D():
530
605
  self.upstreams[curriver] = (up.x, up.y)
531
606
 
532
607
  def _find_upstream(self, curvect:vector) -> wolfvertex:
533
- """ Find the upstream of a vector
534
-
608
+ """ Find the upstream of a vector
609
+
535
610
  :param curvect: The river's axis
536
611
  """
537
612
 
@@ -552,17 +627,17 @@ class Coupling_Hydrology_2D():
552
627
  return vert1
553
628
 
554
629
  def prepare_search(self, rivers:list[str] = None):
555
- """
630
+ """
556
631
  Prepare the search for the hydrology model.
557
632
 
558
- The order is important because the reaches will be
633
+ The order is important because the reaches will be
559
634
  progressively excluded from the search for the next ones.
560
635
 
561
636
  So, you have to start with the **main river** and then the **tributaries**.
562
-
637
+
563
638
  :param rivers: The list of rivers to prepare
564
639
  """
565
-
640
+
566
641
  excluded = []
567
642
 
568
643
  if rivers is None:
@@ -580,12 +655,12 @@ class Coupling_Hydrology_2D():
580
655
 
581
656
  # Récupération de la maille rivière la plus proche
582
657
  dist, node_up = self.river_system.get_nearest_nodes(self.upstreams[curriver])
583
-
658
+
584
659
  # Récupération de la liste des biefs en aval
585
660
  downstream = self.river_system.get_downstream_reaches_excluded(node_up, excluded)
586
661
 
587
662
  excluded += downstream
588
-
663
+
589
664
  # Mis en place d'une structure de recherche rapide
590
665
  nodes, kdtree = self.river_system.get_kdtree_from_reaches(downstream)
591
666
 
@@ -593,37 +668,37 @@ class Coupling_Hydrology_2D():
593
668
 
594
669
  def _is_global(self, col_name:str):
595
670
  """ Vérifie si la colonne est un hydrogramme global """
596
-
671
+
597
672
  return col_name in self.hydrographs_total.columns
598
673
 
599
674
  def _is_partial(self, col_name:str):
600
675
  """ Vérifie si la colonne est un hydrogramme partiel """
601
-
676
+
602
677
  return col_name in self.hydrographs_local.columns
603
678
 
604
679
  def _is_anthropic(self, col_name:str):
605
- """
606
- Vérifie si la colonne est un hydrogramme anthropique
680
+ """
681
+ Vérifie si la colonne est un hydrogramme anthropique
607
682
  (c'est-à-dire une colonne de l'hydrogramme total qui n'est pas un hydrogramme partiel)
608
-
683
+
609
684
  """
610
-
685
+
611
686
  return self._is_global(col_name) and not self._is_partial(col_name)
612
687
 
613
688
  def _is_virtual(self, col_name:str):
614
689
  """ Vérifie si la colonne est un hydrogramme virtuel """
615
-
690
+
616
691
  return col_name in [virtualname for virtualname, src_name, frac, lag in self._hydrographs_virtual]
617
692
 
618
- def _add_infil(self,
619
- type_name:InjectionType,
620
- col_name_q:str | float,
621
- factor:float,
622
- lag:float,
693
+ def _add_infil(self,
694
+ type_name:InjectionType,
695
+ col_name_q:str | float,
696
+ factor:float,
697
+ lag:float,
623
698
  index_zone:int = None):
624
- """
625
- Ajoute une infiltration à la liste des infiltrations
626
-
699
+ """
700
+ Ajoute une infiltration à la liste des infiltrations
701
+
627
702
  :param type_name: nom du type d'infiltration
628
703
  :param col_name: nom de la colonne de l'hydrogramme
629
704
  :param factor: facteur multiplicatif
@@ -646,20 +721,20 @@ class Coupling_Hydrology_2D():
646
721
  if index_zone is None:
647
722
  self.counter_zone_infil += 1
648
723
  index_zone = self.counter_zone_infil
649
-
724
+
650
725
  self.infiltrations.append(Scaled_Infiltration(index_zone, type_name, col_name_q, factor, lag))
651
726
 
652
727
  return index_zone
653
728
 
654
- def _add_local_injecton(self,
655
- local_vect:vector,
656
- type_name:InjectionType,
657
- col_name:str,
658
- factor:float,
729
+ def _add_local_injecton(self,
730
+ local_vect:vector,
731
+ type_name:InjectionType,
732
+ col_name:str,
733
+ factor:float,
659
734
  lag:float):
660
-
661
- """
662
- Ajoute une injection locale à la liste des infiltrations
735
+
736
+ """
737
+ Ajoute une injection locale à la liste des infiltrations
663
738
  et remplissage de la matrice d'infiltration
664
739
 
665
740
  :param local_vect: vecteur de la zone d'injection
@@ -667,7 +742,7 @@ class Coupling_Hydrology_2D():
667
742
  :param col_name: nom de la colonne de l'hydrogramme
668
743
  :param factor: facteur multiplicatif
669
744
  :param lag: déphasage
670
-
745
+
671
746
  """
672
747
 
673
748
  assert type_name in InjectionType, f"Unknown type {type_name}"
@@ -677,7 +752,7 @@ class Coupling_Hydrology_2D():
677
752
 
678
753
  self.local_infiltrations[local_vect] = self._add_infil(type_name, col_name, factor, lag)
679
754
 
680
- # Mise à zéro de la sélection dans la matrice d'infiltration
755
+ # Mise à zéro de la sélection dans la matrice d'infiltration
681
756
  self._infil_idx.SelectionData.reset()
682
757
  # Sélection des mailles à l'intérieur du polygone de la zone d'injection
683
758
  self._infil_idx.SelectionData.select_insidepoly(local_vect)
@@ -687,12 +762,23 @@ class Coupling_Hydrology_2D():
687
762
  # Conversion des coordonnées en indices de mailles
688
763
  ij = self._infil_idx.get_ij_from_xy_array(xy)
689
764
 
690
- # Affectation de l'indice de la zone d'infiltration
691
- for i,j in ij:
692
- self._infil_idx.array[i,j] = self.local_infiltrations[local_vect]
765
+ if self._buildings is not None:
766
+ # Vérification de la présence de bâtiments dans la zone d'infiltration
767
+ for i,j in ij:
768
+ if self._buildings.array[i,j] == 1:
769
+ logging.warning(f"Building found in infiltration zone {local_vect.myname} -- Maille {i,j} ignored")
770
+ continue
771
+ self._infil_idx.array[i,j] = self.local_infiltrations[local_vect]
772
+
773
+ assert np.count_nonzero(self._infil_idx.array == self.local_infiltrations[local_vect]) > 0, f"No infiltration in zone {local_vect.parentzone.myname}"
774
+
775
+ else:
776
+ # Affectation de l'indice de la zone d'infiltration
777
+ for i,j in ij:
778
+ self._infil_idx.array[i,j] = self.local_infiltrations[local_vect]
693
779
 
694
- # Vérification du nombre de mailles affectées
695
- assert len(ij) == np.count_nonzero(self._infil_idx.array == self.local_infiltrations[local_vect]), "Bad count for {}".format(type_name)
780
+ # Vérification du nombre de mailles affectées
781
+ assert len(ij) == np.count_nonzero(self._infil_idx.array == self.local_infiltrations[local_vect]), "Bad count for {}".format(type_name)
696
782
 
697
783
  else:
698
784
  # Une injection existe déjà dans cette zone
@@ -707,8 +793,8 @@ class Coupling_Hydrology_2D():
707
793
  col_name:str,
708
794
  factor:float,
709
795
  lag:float):
710
- """
711
- Ajoute une injection le long de la rivière
796
+ """
797
+ Ajoute une injection le long de la rivière
712
798
  et remplissage de la matrice d'infiltration
713
799
 
714
800
  :param list_part: liste des coordonnées des points de la rivière
@@ -727,22 +813,22 @@ class Coupling_Hydrology_2D():
727
813
 
728
814
  def write_infil_array(self, dirout:Path):
729
815
  """ Sauvegarde de la matrice d'infiltration """
730
-
816
+
731
817
  self._infil_idx.mask_data(0)
732
818
  self._infil_idx.nullvalue = 99999
733
819
  self._infil_idx.set_nullvalue_in_mask()
734
820
  self._infil_idx.write_all(dirout / f'infiltration.tif')
735
821
 
736
822
  def _get_reaches_in_sub(self, subbasin:SubWatershed, rivers_names:list[str]) -> list[list[int]]:
737
- """
738
- Retourne une liste de listes des biefs dans le sous-bassin
739
-
823
+ """
824
+ Retourne une liste de listes des biefs dans le sous-bassin
825
+
740
826
  :param rivers: liste des noms des rivières
741
827
  :return: liste des biefs dans le sous-bassin
742
828
  """
743
829
 
744
830
  ret = []
745
-
831
+
746
832
  if rivers_names[0] not in self._searching:
747
833
  logging.error(f"River {rivers_names[0]} not found")
748
834
  return ret
@@ -750,7 +836,7 @@ class Coupling_Hydrology_2D():
750
836
  reaches1 = self._searching[rivers_names[0]].downstream_reaches
751
837
 
752
838
  reaches_in_sub = [idx for idx in reaches1 if subbasin.is_reach_in_sub(idx)]
753
-
839
+
754
840
  if len(reaches_in_sub) == 0:
755
841
  logging.error(f"No reaches in subbasin for river {rivers_names[0]}")
756
842
 
@@ -762,7 +848,7 @@ class Coupling_Hydrology_2D():
762
848
  for loc in locret:
763
849
  ret.append(loc)
764
850
  else:
765
-
851
+
766
852
  if rivers_names[1] not in self._searching:
767
853
  logging.error(f"River {rivers_names[1]} not found")
768
854
  return ret
@@ -778,9 +864,9 @@ class Coupling_Hydrology_2D():
778
864
  return ret
779
865
 
780
866
  def _get_outlet_reaches(self, subbasin:SubWatershed, idx_reaches:list[int]) -> Node_Watershed:
781
- """
782
- Retourne le noeud de sortie du sous-bassin
783
-
867
+ """
868
+ Retourne le noeud de sortie du sous-bassin
869
+
784
870
  :param reaches: liste des biefs dans le sous-bassin
785
871
  :return: noeud de sortie du sous-bassin
786
872
  """
@@ -800,16 +886,16 @@ class Coupling_Hydrology_2D():
800
886
  return newsub1, newsub2
801
887
 
802
888
  def _split_hydrographs(self, subbasin:SubWatershed | str, river_names:list[str, list[str]]):
803
- """
804
- Séparation de l'hydrogramme partiel en fonction
805
- des surfaces drainées par chaque rivère
806
-
889
+ """
890
+ Séparation de l'hydrogramme partiel en fonction
891
+ des surfaces drainées par chaque rivère
892
+
807
893
  On attend au maximum 2 rivières ou 1 rivière et une liste de rivières.
808
894
 
809
895
  Les rivières seront traitées 2 par 2 de façon récursive.
810
896
 
811
897
  La seconde rivière et l'affluent de la première rivière.
812
-
898
+
813
899
  """
814
900
 
815
901
  if isinstance(subbasin, str):
@@ -821,7 +907,7 @@ class Coupling_Hydrology_2D():
821
907
  fraction2 = sub2.area / subbasin.area
822
908
 
823
909
  assert fraction1 + fraction2 == 1., "Bad fractions"
824
-
910
+
825
911
  self.add_virtual_hydrograph(sub1.name, subbasin.name, fraction1, 0.)
826
912
  self.add_virtual_hydrograph(sub2.name, subbasin.name, fraction2, 0.)
827
913
 
@@ -835,18 +921,18 @@ class Coupling_Hydrology_2D():
835
921
  added.append((sub2.name, river_names[1]))
836
922
 
837
923
  return added
838
-
924
+
839
925
  def get_locale_injection_names(self):
840
926
  """ Print the names of the local injections """
841
-
927
+
842
928
  return list(self.locale_injections.keys())
843
-
929
+
844
930
  def get_along_injection_names(self) -> tuple[list[str], list[str]]:
845
- """ Get the names of the along injections
846
-
931
+ """ Get the names of the along injections
932
+
847
933
  :return: The names of the rivers along which the injections are made and the columns of the hydrographs
848
934
  """
849
-
935
+
850
936
  return list(self.lists_part.keys()), list(self.hydrographs_local.columns)
851
937
 
852
938
  def reset_injections(self):
@@ -858,7 +944,7 @@ class Coupling_Hydrology_2D():
858
944
  self._infil_idx.array[:,:] = 0
859
945
 
860
946
  self._hydrographs_virtual = []
861
-
947
+
862
948
  self.infiltrations = []
863
949
  self.local_infiltrations = {}
864
950
 
@@ -871,7 +957,7 @@ class Coupling_Hydrology_2D():
871
957
  self.injections_along()
872
958
 
873
959
  self.create_hydrographs()
874
-
960
+
875
961
  def injections_locales(self, couplings:list[tuple[str, str, InjectionType]] = None):
876
962
  """ Ajoute les injections locales """
877
963
 
@@ -890,9 +976,9 @@ class Coupling_Hydrology_2D():
890
976
 
891
977
 
892
978
  def link_area2nodes(self):
893
- """
979
+ """
894
980
  Searching cells in dem associated to the river nodes in the hydrological model.
895
-
981
+
896
982
  We use the river axis to select the cells in the dem.
897
983
 
898
984
  Then we search the nearest river nodes in the hydrological
@@ -901,7 +987,7 @@ class Coupling_Hydrology_2D():
901
987
  We create local lists of cells associated to one river node.
902
988
 
903
989
  Due to the fact that the river axis is not exactly the same
904
- as the river nodes (not the same spatial resolution, rester vs vector),
990
+ as the river nodes (not the same spatial resolution, rester vs vector),
905
991
  all river nodes in the hydrological model
906
992
  are not necessarely associated to cells in the dem.
907
993
 
@@ -914,22 +1000,22 @@ class Coupling_Hydrology_2D():
914
1000
  nodes = self._searching[key_river].nodes
915
1001
 
916
1002
  # Mise à 0 des zones sélectionnées
917
- self._dem.SelectionData.reset()
1003
+ self._dem.SelectionData.reset()
918
1004
  # Sélection des mailles sur l'axe du lit mineur
919
1005
  self._dem.SelectionData.select_underpoly(river_axis)
920
1006
 
921
1007
  # Coordonnées XY des mailles sélectionnées
922
1008
  xy_selected = self._dem.SelectionData.myselection
923
-
924
- # Recherche des mailles rivières les plus proches
1009
+
1010
+ # Recherche des mailles rivières les plus proches
925
1011
  # dans la modélisation hydrologique
926
1012
  dist, nearest = kdtree.query(xy_selected, k=1)
927
1013
  # Récupération des noeuds correspondants aux index fournis par l'objet KDTree
928
1014
  nearest:list[Node_Watershed] = [nodes[i] for i in nearest]
929
-
1015
+
930
1016
  # Surface drainée par les mailles
931
1017
  drained_surface = np.array([cur.uparea for cur in nearest])
932
-
1018
+
933
1019
  # Valeurs de BV uniques
934
1020
  unique_area = np.unique(drained_surface)
935
1021
 
@@ -950,7 +1036,7 @@ class Coupling_Hydrology_2D():
950
1036
  if along is None:
951
1037
  logging.error(_("No along injections defined"))
952
1038
  return
953
-
1039
+
954
1040
  along = along.copy()
955
1041
 
956
1042
  # ## Création de bassins virtuels afin de séparer les hydrogrammes en plusieurs rivières
@@ -964,7 +1050,7 @@ class Coupling_Hydrology_2D():
964
1050
 
965
1051
  # La rivière secondaire peut également être décomposée en plusieurs selon le même principe.
966
1052
 
967
- # **La procédure de calcul est récursive.**
1053
+ # **La procédure de calcul est récursive.**
968
1054
  to_split = [cur for cur in along if isinstance(cur[1], list)]
969
1055
 
970
1056
  replace = []
@@ -975,16 +1061,16 @@ class Coupling_Hydrology_2D():
975
1061
  along.remove(cur)
976
1062
 
977
1063
  for cur in replace:
978
- along.append(cur)
1064
+ along.append(cur)
979
1065
 
980
1066
  for cur in along:
981
- self._injection_along(cur)
982
-
1067
+ self._injection_along(cur)
1068
+
983
1069
  def _injection_along(self, name_subwatershed_river:tuple[str, str]):
984
1070
 
985
- # Nom de colonne et liste de mailles potentielles à utiliser
1071
+ # Nom de colonne et liste de mailles potentielles à utiliser
986
1072
  # pour la répartition
987
-
1073
+
988
1074
  name_subwatershed, river = name_subwatershed_river
989
1075
 
990
1076
  list_rivers, used_reaches = self.lists_part[river], self._searching[river].downstream_reaches
@@ -1006,14 +1092,14 @@ class Coupling_Hydrology_2D():
1006
1092
  local_areas = 0.
1007
1093
  # liste contenant la maille de connection au réseau et la maille d'injection locale
1008
1094
  to_remove:list[tuple[Node_Watershed, Node_Watershed, float]] = []
1009
-
1095
+
1010
1096
  for cur_locinj in local_injections:
1011
1097
 
1012
1098
  # Recherche du noeud rivière le plus proche de la zone d'injection locale
1013
1099
  dist, node_local_injection = self.river_system.get_nearest_nodes(cur_locinj)
1014
1100
 
1015
1101
  if node_local_injection.reach not in used_reaches:
1016
- # Recherche de la maille rivière en aval
1102
+ # Recherche de la maille rivière en aval
1017
1103
  # qui fait partie de la distribution en long
1018
1104
  down = self.river_system.go_downstream_until_reach_found(node_local_injection, used_reaches)
1019
1105
  else:
@@ -1022,8 +1108,8 @@ class Coupling_Hydrology_2D():
1022
1108
  down = node_local_injection
1023
1109
  while down is not None and down.uparea not in unique_areas:
1024
1110
  down = down.down
1025
-
1026
-
1111
+
1112
+
1027
1113
  # surface su sous-bassin qui sera injectée localement
1028
1114
  local_area = node_local_injection.uparea - subbasin.get_area_outside_sub_if_exists(node_local_injection, node_local_injection.get_up_reaches_same_sub())
1029
1115
 
@@ -1043,7 +1129,7 @@ class Coupling_Hydrology_2D():
1043
1129
 
1044
1130
  # aire drainée à la limite amont du ss-bassin, le long de la distribution en long
1045
1131
  up_node = subbasin.get_up_rivernode_outside_sub(subbasin.outlet, used_reaches)
1046
-
1132
+
1047
1133
  if up_node is None:
1048
1134
  starting_node = subbasin.get_list_nodes_river(min(used_reaches))[-1]
1049
1135
  area_min = subbasin.get_area_outside_sub_if_exists(starting_node, starting_node.get_up_reaches_same_sub())
@@ -1059,7 +1145,7 @@ class Coupling_Hydrology_2D():
1059
1145
  frac_sum=0.
1060
1146
 
1061
1147
  def area_to_remove(node:Node_Watershed) -> float:
1062
-
1148
+
1063
1149
  uparea = 0.
1064
1150
 
1065
1151
  # injections locales
@@ -1086,10 +1172,10 @@ class Coupling_Hydrology_2D():
1086
1172
 
1087
1173
  if fraction_loc > 0.:
1088
1174
 
1089
- self._add_along_injection(lists[unique_areas[idx]],
1090
- InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1091
- name_subwatershed,
1092
- fraction_loc,
1175
+ self._add_along_injection(lists[unique_areas[idx]],
1176
+ InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1177
+ name_subwatershed,
1178
+ fraction_loc,
1093
1179
  lag =0.)
1094
1180
 
1095
1181
  frac_sum += fraction_loc
@@ -1100,26 +1186,26 @@ class Coupling_Hydrology_2D():
1100
1186
 
1101
1187
  fraction_loc = delta_loc / delta_area
1102
1188
 
1103
- self._add_along_injection(lists[unique_areas[-1]],
1104
- InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1105
- name_subwatershed,
1106
- fraction_loc,
1189
+ self._add_along_injection(lists[unique_areas[-1]],
1190
+ InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1191
+ name_subwatershed,
1192
+ fraction_loc,
1107
1193
  lag =0.)
1108
1194
 
1109
1195
  frac_sum += fraction_loc
1110
1196
 
1111
1197
  if frac_sum > 1.001 or frac_sum < 0.999:
1112
- logging.error(f"Bad sum of fractions {frac_sum} " + name_subwatershed)
1198
+ logging.error(f"Bad sum of fractions {frac_sum} " + name_subwatershed)
1113
1199
 
1114
1200
 
1115
1201
  def create_hydrographs(self):
1116
- """ Création des hydrogrammes
1117
-
1202
+ """ Création des hydrogrammes
1203
+
1118
1204
  Les étapes précédentes ont ajouté à la liste "infiltrations" les éléments suivants:
1119
1205
 
1120
1206
  - l'index de la zone d'infiltration (1-based)
1121
1207
  - l'hydrogramme de référence
1122
- - la fecteur pondérateur
1208
+ - le facteur pondérateur
1123
1209
  - le temps de déphasage
1124
1210
 
1125
1211
  Une zone peut contenir plusieurs apports.
@@ -1128,7 +1214,7 @@ class Coupling_Hydrology_2D():
1128
1214
 
1129
1215
  Le fichier final est ordonné comme la matrice d'infiltration.
1130
1216
 
1131
- Avant de sommer, il faut tout d'abord créer les hydrogrammes associés au BV virtuels (décomposition d'un BV, modélisé comme un tout, en plusieurs rivières distinctes pour la répartition en long)
1217
+ Avant de sommer, il faut tout d'abord créer les hydrogrammes associés au BV virtuels (décomposition d'un BV, modélisé comme un tout, en plusieurs rivières distinctes pour la répartition en long)
1132
1218
  """
1133
1219
 
1134
1220
  dt = self.hydrographs_total.index[1] - self.hydrographs_total.index[0]
@@ -1146,7 +1232,7 @@ class Coupling_Hydrology_2D():
1146
1232
 
1147
1233
  if src_hydrograph_name in self.hydrographs_local.columns:
1148
1234
  df_virtual[name] = self.hydrographs_local.shift(decal, fill_value=0.)[src_hydrograph_name] * factor
1149
-
1235
+
1150
1236
  elif src_hydrograph_name in df_virtual.columns:
1151
1237
  df_virtual[name] = df_virtual.shift(decal, fill_value=0.)[src_hydrograph_name] * factor
1152
1238
 
@@ -1176,26 +1262,26 @@ class Coupling_Hydrology_2D():
1176
1262
  loc_count += 1
1177
1263
 
1178
1264
  if type_name == InjectionType.GLOBAL:
1179
-
1265
+
1180
1266
  if loc_count == 1:
1181
1267
  df_2d_dict[idx] = self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1182
1268
  else:
1183
1269
  df_2d_dict[idx] += self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1184
-
1270
+
1185
1271
  elif type_name == InjectionType.PARTIAL:
1186
-
1272
+
1187
1273
  if loc_count == 1:
1188
1274
  df_2d_dict[idx] = self.hydrographs_local.shift(decal, fill_value = 0.)[col_name] * factor
1189
1275
  else:
1190
1276
  df_2d_dict[idx] += self.hydrographs_local.shift(decal, fill_value = 0.)[col_name] * factor
1191
-
1277
+
1192
1278
  elif type_name == InjectionType.ANTHROPOGENIC:
1193
1279
 
1194
1280
  if loc_count == 1:
1195
1281
  df_2d_dict[idx] = self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1196
1282
  else:
1197
1283
  df_2d_dict[idx] += self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1198
-
1284
+
1199
1285
  elif type_name == InjectionType.VIRTUAL:
1200
1286
 
1201
1287
  if loc_count == 1:
@@ -1210,9 +1296,17 @@ class Coupling_Hydrology_2D():
1210
1296
  else:
1211
1297
  df_2d_dict[idx] += col_name * factor
1212
1298
 
1299
+ elif type_name == InjectionType.FORCED_UNSTEADY:
1300
+
1301
+ col_name:pd.Series
1302
+ if loc_count == 1:
1303
+ df_2d_dict[idx] = col_name.loc[self.dateBegin:self.dateEnd].values * factor
1304
+ else:
1305
+ df_2d_dict[idx] += col_name.loc[self.dateBegin:self.dateEnd].values * factor
1306
+
1213
1307
  else:
1214
1308
  logging.error(f"Unknown type {type_name}")
1215
-
1309
+
1216
1310
  if loc_count != counter[i-1]:
1217
1311
  logging.error(f"Bad count for {i}")
1218
1312
 
@@ -1221,9 +1315,18 @@ class Coupling_Hydrology_2D():
1221
1315
 
1222
1316
 
1223
1317
  def save_hydrographs(self, dirout:Path, name:str):
1224
- """ Write the hydrographs"""
1318
+ """ Write the hydrographs
1225
1319
 
1226
- with open(dirout / (name + f'_infiltration_zones.txt'), 'w') as f:
1320
+ :param dirout: The output directory
1321
+ :param name: The name of the output file (if no suffix .txt, it will be added)
1322
+ """
1323
+
1324
+ # ensure suffix .txt
1325
+ if not name.endswith('.txt'):
1326
+ name += '.txt'
1327
+
1328
+ locname = name.replace('.txt', '_infiltration_zones.txt')
1329
+ with open(dirout / locname, 'w') as f:
1227
1330
  f.write("Zone\tType\tColonne\tFacteur\tLag\n")
1228
1331
  for cur in self.infiltrations:
1229
1332
  idx, type_name, col_name, factor, lag = cur.index, cur.type.value, cur.colref, cur.factor, cur.lagtime
@@ -1232,5 +1335,5 @@ class Coupling_Hydrology_2D():
1232
1335
  if self.df_2d is None:
1233
1336
  logging.error("No hydrographs created")
1234
1337
  return
1235
-
1236
- self.df_2d.to_csv(dirout / name, sep='\t', decimal='.', encoding='latin1')
1338
+
1339
+ self.df_2d.to_csv(dirout / name, sep='\t', decimal='.', encoding='latin1')