wolfhece 2.1.21__py3-none-any.whl → 2.1.23__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.
@@ -0,0 +1,1226 @@
1
+ from ..hydrology.Catchment import Catchment, SubBasin
2
+ from ..hydrology.PyWatershed import Watershed, Node_Watershed, RiverSystem, SubWatershed
3
+ from ..wolf_array import WolfArray, WOLF_ARRAY_FULL_INTEGER, WOLF_ARRAY_FULL_SINGLE
4
+ from ..PyVertexvectors import Zones,zone,vector, wolfvertex
5
+ from ..PyTranslate import _
6
+
7
+ from scipy.spatial import KDTree
8
+ import pandas as pd
9
+ from pathlib import Path
10
+ import numpy as np
11
+ from typing import Literal, Union
12
+ from enum import Enum
13
+ import logging
14
+ import matplotlib.pyplot as plt
15
+ from matplotlib.figure import Figure
16
+ from matplotlib.axes import Axes
17
+
18
+
19
+ class InjectionType(Enum):
20
+ GLOBAL = 'Global'
21
+ PARTIAL = 'Partial'
22
+ ANTHROPOGENIC = 'Anthropogenic'
23
+ CONSTANT = 'Constant'
24
+ VIRTUAL = 'Virtual'
25
+
26
+
27
+ class Searching_Context():
28
+
29
+ def __init__(self,
30
+ river_axis:vector,
31
+ kdtree:KDTree,
32
+ nodes:Node_Watershed,
33
+ downstream_reaches:list[int],
34
+ up_node:Node_Watershed) -> None:
35
+
36
+ self.river_axis:vector = river_axis # river axis -- vector
37
+ self.kdtree:KDTree = kdtree # KDTree of the downstream reaches
38
+ self.nodes:Node_Watershed = nodes # nodes of the downstream reaches - from Hydrology model
39
+ self.downstream_reaches:list[int] = downstream_reaches# downstream reaches - from Hydrology model
40
+ self.up_node:Node_Watershed = up_node # up node - from Hydrology model
41
+
42
+ def __str__(self) -> str:
43
+
44
+ ret = ''
45
+
46
+ ret += f" Number of reaches: {len(self.downstream_reaches)}\n"
47
+ ret += f" Downstream reaches: {self.downstream_reaches}\n"
48
+ ret += f" Number of nodes: {len(self.nodes)}\n"
49
+ ret += f" Length of the river axis: {self.river_axis.length2D}\n"
50
+
51
+ return ret
52
+
53
+
54
+ class Scaled_Infiltration():
55
+
56
+ def __init__(self, idx:int, type:InjectionType, colref:str, factor:float, lagtime:float) -> None:
57
+
58
+ self.index = idx # index of the infiltration
59
+ self.type = type # type of the infiltration
60
+ self.colref = colref # reference column
61
+ self.factor = factor # multiplicator factor [-]
62
+ self.lagtime = lagtime # lag time [s]
63
+
64
+ class Coupling_Hydrology_2D():
65
+
66
+ def __init__(self) -> None:
67
+
68
+
69
+ self._rivers_zones:list[Zones] = []
70
+ self.rivers:dict[str, vector] = {}
71
+
72
+ self.upstreams:dict[str, wolfvertex] = {}
73
+
74
+ self._locale_injection_zones:Zones = None
75
+ self.locale_injections:dict[str, vector] = {}
76
+
77
+ self._hydrology_model:Catchment = None
78
+
79
+ self._searching:dict[str, Searching_Context] = {}
80
+
81
+ self.hydrographs_total:pd.DataFrame = None
82
+ self.hydrographs_local:pd.DataFrame = None
83
+ self._hydrographs_virtual = []
84
+
85
+ self._dem:WolfArray = None
86
+ self._infil_idx:WolfArray = None
87
+ self.counter_zone_infil = 0
88
+
89
+ # Les infiltrations sont définies par des zones de mailles.
90
+ # Chaque zone est définie par :
91
+ # - un type d'infiltration
92
+ # - un nom d'hydrogramme (ou une valeur constante)
93
+ # - un facteur podérateur
94
+ # - un temps de déphasage
95
+
96
+ self.infiltrations:list[Scaled_Infiltration] = []
97
+ self.local_infiltrations:dict[vector:int] = {}
98
+
99
+ # lists part is a dictionary with the following structure:
100
+ # - key : river name
101
+ # - value : tuple with two elements
102
+ # - first element : list of unique drained areas
103
+ # - second element : dictionary with the following structure
104
+ # - key : drained area
105
+ # - value : list of coordinates of the river points in the dem
106
+ self.lists_part:dict[str, tuple[np.ndarray, dict[float, list[tuple[float, float]]]]] = {}
107
+
108
+ self.df_2d = None
109
+
110
+ self_along = None
111
+ self._locales = None
112
+
113
+ def __str__(self) -> str:
114
+ """ Return a string representation of the coupling """
115
+
116
+ ret =''
117
+
118
+ ret += _("Rivers: {}\n").format(len(self.rivers))
119
+ for curriver in self.rivers:
120
+ ret += f"{curriver}\n"
121
+
122
+ ret += _("Local injections: {}\n").format(len(self.locale_injections))
123
+ for curinj in self.locale_injections:
124
+ ret += f"{curinj}\n"
125
+
126
+
127
+ ret += _("Upstreams: {}\n").format(len(self.upstreams))
128
+ for curup in self.upstreams:
129
+ ret += f" {curup} :\n"
130
+ ret += f" X : {self.upstreams[curup][0]}\n"
131
+ ret += f" Y : {self.upstreams[curup][1]}\n"
132
+ ret += f" Drained area : {self.river_system.get_nearest_nodes(self.upstreams[curup])[1].uparea}\n"
133
+
134
+ for cursearch in self._searching:
135
+ ret += f"{cursearch} : \n{self._searching[cursearch]}\n"
136
+
137
+ ret += f"Coupling array: \n{self._infil_idx}\n"
138
+
139
+ return ret
140
+
141
+ @property
142
+ def number_of_injections(self) -> int:
143
+
144
+ if self._infil_idx is None:
145
+ logging.error(_("No infiltration array -- Spread the injections first"))
146
+ return 0
147
+
148
+ return self._infil_idx.array.max()
149
+
150
+ @property
151
+ def number_of_nodes_per_zone(self) -> dict[int, int]:
152
+ """ Return the number of nodes per zone """
153
+
154
+ if self._infil_idx is None:
155
+ logging.error(_("No infiltration array -- Spread the injections first"))
156
+ return {}
157
+
158
+ non_zeros = self._infil_idx.array[np.where(self._infil_idx.array > 0)]
159
+
160
+ return {i:np.count_nonzero(non_zeros == i) for i in range(1, self.number_of_injections+1)}
161
+
162
+ def plot_number_of_nodes_per_zone(self) -> tuple[Figure, Axes]:
163
+ """ Plot the number of nodes per zone """
164
+
165
+ if self._infil_idx is None:
166
+ logging.error(_("No infiltration array -- Spread the injections first"))
167
+ return None, None
168
+
169
+ fig, ax = plt.subplots()
170
+ ax.hist(self.number_of_nodes_per_zone)
171
+ ax.set_title(_("Number of nodes per zone"))
172
+ ax.set_xlabel(_("Zone index"))
173
+ ax.set_ylabel(_("Number of nodes"))
174
+
175
+ return fig, ax
176
+
177
+ @property
178
+ def along(self) -> list[str, str]:
179
+ return self._along
180
+
181
+ @along.setter
182
+ def along(self, value:list[tuple[str, str]]) -> None:
183
+
184
+ for curval in value:
185
+
186
+ curcol, currivers = curval
187
+
188
+ assert curcol in self.hydrographs_local.columns, f"Column {curcol} not found in hydrographs"
189
+
190
+ def check_rivers(currivers):
191
+ if isinstance(currivers, list):
192
+ for curriver in currivers:
193
+ check_rivers(curriver)
194
+ else:
195
+ assert currivers in self.rivers, f"River {currivers} not found"
196
+
197
+ self._along = value
198
+
199
+ @property
200
+ def locales(self) -> list[str]:
201
+ return self._locales
202
+
203
+ @locales.setter
204
+ def locales(self, value:list[str]) -> None:
205
+
206
+ for curvect, curcol, curtype in value:
207
+
208
+ if curtype != InjectionType.CONSTANT:
209
+ assert curvect in self.locale_injections, f"Vector {curvect} not found"
210
+ assert curcol in list(self.hydrographs_total.columns)+ self._hydrographs_virtual, f"Column {curcol} not found in hydrographs"
211
+
212
+ self._locales = value
213
+
214
+ @property
215
+ def watershed(self) -> Watershed:
216
+ return self._hydrology_model.charact_watrshd
217
+
218
+ @property
219
+ def river_system(self) -> RiverSystem:
220
+ return self.watershed.riversystem
221
+
222
+ @property
223
+ def subs_array(self) -> WolfArray:
224
+ return self.watershed.subs_array
225
+
226
+ def set_array_to_coupling(self, array:WolfArray | Path) -> None:
227
+ """ Set the array to coupling
228
+
229
+ :param array: The array to coupling
230
+ """
231
+
232
+ if isinstance(array, Path):
233
+ if array.exists():
234
+ self._dem = WolfArray(array)
235
+ else:
236
+ logging.error(_("File {} not found").format(array))
237
+ return
238
+
239
+ self._dem = array
240
+ self._create_infiltration_array()
241
+
242
+ def _create_infiltration_array(self):
243
+ """ Create the infiltration array """
244
+
245
+ if self._dem is None:
246
+ logging.error(_("No array to coupling"))
247
+ return
248
+
249
+ self._infil_idx = WolfArray(srcheader=self._dem.get_header(), whichtype=WOLF_ARRAY_FULL_INTEGER)
250
+ self._infil_idx.add_ops_sel()
251
+ self._infil_idx.array[:,:] = 0
252
+
253
+ self.counter_zone_infil = 0
254
+
255
+ def add_hydrology_model(self, name:str, filename:str | Path) -> None:
256
+ """ Add a hydrology model to the coupling
257
+
258
+ :param filename: The filename of the hydrology model
259
+ """
260
+
261
+ self._hydrology_model = Catchment(name, str(filename), False, True)
262
+
263
+ def get_anthropogenic_names(self) -> list[str]:
264
+ """ Print the names of the anthropogenic hydrographs """
265
+
266
+ if self._hydrology_model is None:
267
+ logging.error(_("No hydrology model loaded"))
268
+ return []
269
+
270
+ return [" : ".join([cur_anth.name, name])
271
+ for cur_anth in self._hydrology_model.retentionBasinDict.values()
272
+ for name in cur_anth.get_outFlow_names()]
273
+
274
+ def get_names_areas(self) -> list[str]:
275
+ """ Print the names of the areas """
276
+
277
+ if self._hydrology_model is None:
278
+ logging.error(_("No hydrology model loaded"))
279
+ return []
280
+
281
+ names= [cur_sub.name for cur_sub in sorted(self._hydrology_model.subBasinDict.values(), key=lambda sub: sub.iDSorted)]
282
+ area_subs = [cur_sub.surfaceDrained for cur_sub in sorted(self._hydrology_model.subBasinDict.values(), key=lambda sub: sub.iDSorted)]
283
+ area_glob = [cur_sub.surfaceDrainedHydro for cur_sub in sorted(self._hydrology_model.subBasinDict.values(), key=lambda sub: sub.iDSorted)]
284
+
285
+ return names, area_subs, area_glob
286
+
287
+ def create_hydrographs_local_global(self, unit_discharge:float, total_duration:float, anth_discharge:dict = None) -> None:
288
+ """
289
+ Create the hydrographs from the hydrology model .
290
+
291
+ Global and local hydrographs are created based on a unit discharge and a total duration.
292
+
293
+ You can also add anthropogenic hydrographs from a dictionary.
294
+ The key is the name of the anthropogenic hydrograph and the value is the discharge.
295
+ The keys can be obtained with the method get_anthropogenic_names.
296
+
297
+ :param unit_discharge: The discharge per square kilometer [m³/s/km²]
298
+ :param total_duration: The total duration of the hydrographs [s]
299
+ """
300
+
301
+ # Extract the column names according to their sorted subbasin indices
302
+ col_time = "Time [s]"
303
+ col_subs, area_subs, area_glob = self.get_names_areas()
304
+ col_anth = [" : ".join([cur_anth.name, name])
305
+ for cur_anth in self._hydrology_model.retentionBasinDict.values()
306
+ for name in cur_anth.get_outFlow_names()]
307
+
308
+ #Create a dictionnary
309
+
310
+ dict_glob = {col_time : [0., total_duration]}
311
+
312
+ for cur_sub, cur_area in zip(col_subs, area_glob):
313
+ discharge = cur_area * unit_discharge
314
+ dict_glob[cur_sub] = [discharge, discharge]
315
+
316
+ for cur_anth in col_anth:
317
+ dict_glob[cur_anth] = [0., 0.]
318
+
319
+ if anth_discharge is not None:
320
+ for cur_anth, cur_discharge in anth_discharge.items():
321
+ dict_glob[cur_anth] = [cur_discharge, cur_discharge]
322
+
323
+ dict_loc = {col_time : [0., total_duration]}
324
+
325
+ for cur_sub, cur_area in zip(col_subs, area_subs):
326
+ discharge = cur_area * unit_discharge
327
+ dict_loc[cur_sub] = [discharge, discharge]
328
+
329
+ self.hydrographs_total = pd.DataFrame(dict_glob)
330
+ self.hydrographs_local = pd.DataFrame(dict_loc)
331
+
332
+ self.hydrographs_local.set_index(col_time, inplace=True)
333
+ self.hydrographs_total.set_index(col_time, inplace=True)
334
+
335
+ def save_hydrographs(self, directory:str | Path = None, total:str = None, partial:str = None) -> None:
336
+ """ Write the hydrographs from the hydrology model """
337
+
338
+ if directory is None:
339
+
340
+ if self._hydrology_model is None:
341
+ logging.error(_("No hydrology model loaded"))
342
+ return
343
+
344
+ directory = Path(self._hydrology_model.workingDir) / 'PostProcess'
345
+
346
+ if total is not None:
347
+ self.hydrographs_total.to_csv(directory / total, sep='\t', decimal='.', encoding='latin1')
348
+ else:
349
+ self.hydrographs_total.to_csv(directory / 'Hydros_2_simul2D.txt', sep='\t', decimal='.', encoding='latin1')
350
+
351
+ if partial is not None:
352
+ self.hydrographs_local.to_csv(directory / partial, sep='\t', decimal='.', encoding='latin1')
353
+ else:
354
+ self.hydrographs_local.to_csv(directory / 'HydrosSub_2_simul2D.txt', sep='\t', decimal='.', encoding='latin1')
355
+
356
+
357
+ def load_hydrographs(self, directory:str | Path = None, total:str = None, partial:str = None) -> None:
358
+ """ Load the hydrographs from the hydrology model
359
+
360
+ :param directory: The directory of the hydrology model -- If None, the working directory of the loaded hydrology model is used
361
+ :param total: The filename of the total hydrographs - If None, the default filename is used
362
+ :param partial: The filename of the partial hydrographs - If None, the default filename is used
363
+
364
+ """
365
+
366
+ if directory is None:
367
+
368
+ if self._hydrology_model is None:
369
+ logging.error(_("No hydrology model loaded"))
370
+ return
371
+
372
+ directory = Path(self._hydrology_model.workingDir) / 'PostProcess'
373
+
374
+ if total is not None:
375
+ total = directory / total
376
+ if total.exists():
377
+ self.hydrographs_total = pd.read_csv(total, sep='\t', decimal='.', header=0, index_col=0, encoding='latin1')
378
+ else:
379
+ logging.error(_("File {} not found").format(total))
380
+
381
+ else:
382
+ total = directory / 'Hydros_2_simul2D.txt'
383
+ if total.exists():
384
+ self.hydrographs_total = pd.read_csv(total, sep='\t', decimal='.', header=0, index_col=0, encoding='latin1')
385
+ else:
386
+ logging.error(_("File {} not found").format(total))
387
+
388
+ if partial is not None:
389
+ partial = directory / partial
390
+ if partial.exists():
391
+ self.hydrographs_local = pd.read_csv(partial, sep='\t', decimal='.', header=0, index_col=0, encoding='latin1')
392
+ else:
393
+ logging.error(_("File {} not found").format(partial))
394
+
395
+ else:
396
+ partial = directory / 'HydrosSub_2_simul2D.txt'
397
+ if partial.exists():
398
+ self.hydrographs_local = pd.read_csv(partial, sep='\t', decimal='.', header=0, index_col=0, encoding='latin1')
399
+ else:
400
+ logging.error(_("File {} not found").format(partial))
401
+
402
+
403
+ def print_hydrographs(self, total:bool = True, partial:bool = True) -> None:
404
+ """ Print the hydrographs from the hydrology model """
405
+
406
+ if total:
407
+ print(_("Total hydrographs:"))
408
+ print(self.hydrographs_total.columns)
409
+
410
+ if partial:
411
+ print(_("Partial hydrographs:"))
412
+ print(self.hydrographs_local.columns)
413
+
414
+ def plot_hydrographs(self, total:bool = True, partial:bool = True) -> tuple[tuple[Figure, Axes],tuple[Figure, Axes]]:
415
+ """ Plot the hydrographs from the hydrology model """
416
+
417
+ if total:
418
+ ax1 = self.hydrographs_total.plot()
419
+ fig1 = ax1.figure
420
+ ax1.legend(loc='upper center', ncol=8)
421
+ ax1.set_ylim(0, 1000)
422
+ fig1.set_size_inches(15, 5)
423
+ fig1.tight_layout()
424
+ else:
425
+ fig1, ax1 = None, None
426
+
427
+ if partial:
428
+ ax2 = self.hydrographs_local.plot()
429
+ fig2 = ax2.figure
430
+ ax2.legend(loc='upper center', ncol=8)
431
+ ax2.set_ylim(0, 1000)
432
+ fig2.set_size_inches(15, 5)
433
+ fig2.tight_layout()
434
+ else:
435
+ fig2, ax2 = None, None
436
+
437
+ return (fig1, ax1), (fig2, ax2)
438
+
439
+
440
+ def add_virtual_hydrograph(self, name:str, src_hydrograph_name:str, factor:float, lag:float=0.):
441
+ """ Add a virtual hydrograph to the hydrology model """
442
+
443
+ self._hydrographs_virtual.append((name, src_hydrograph_name, factor, lag))
444
+
445
+ def add_river(self, filename:str | Path) -> None:
446
+ """ Add a river to the hydrology model
447
+
448
+ :param filename: The filename of the river
449
+ """
450
+
451
+ self._rivers_zones.append(Zones(filename))
452
+
453
+ def reset(self) -> None:
454
+ """ Reset the hydrology model """
455
+
456
+ self._rivers_zones = []
457
+ self.rivers = {}
458
+ self._locale_injection_zones = None
459
+ self.locale_injections = {}
460
+ self.lists_part = {}
461
+ self.df_2d = None
462
+
463
+ self._searching = {}
464
+
465
+ self.reset_injections()
466
+
467
+ def add_locale_injections(self, filename:str | Path) -> None:
468
+ """ Add a local injection to the hydrology model
469
+
470
+ :param filename: The filename of the local injection
471
+ """
472
+
473
+ self._locale_injection_zones = Zones(filename)
474
+
475
+ def find_river_axis(self):
476
+ """ Find the river axis from Zones """
477
+
478
+ for curriver in self._rivers_zones:
479
+ for curzone in curriver.myzones:
480
+ if curzone.nbvectors !=3:
481
+ logging.error(_("Zone {} has {} vectors, should be 3").format(curzone.myname, curzone.nbvectors))
482
+ else:
483
+ # Select the river axis
484
+ curvector = curzone.myvectors[1]
485
+ if curvector.used:
486
+ self.rivers[curzone.myname] = curvector
487
+ else:
488
+ logging.warning(_("Vector {} in zone {} is not used -- Ignoring it as a river axis").format(curvector.myname, curzone.myname))
489
+
490
+ def _add_injection(self, name:str, vect:vector):
491
+ """ Add an injection to the hydrology model
492
+
493
+ :param name: The name of the injection
494
+ :param vect: The vector of the injection
495
+ """
496
+
497
+ self.locale_injections[name] = vect
498
+
499
+ def find_injections(self):
500
+ """ Find the injection points from Zones """
501
+
502
+ for curzone in self._locale_injection_zones.myzones:
503
+
504
+ names = [curvect.myname for curvect in curzone.myvectors]
505
+
506
+ if 'injection' in names:
507
+ vect = curzone.myvectors[names.index('injection')]
508
+ if vect.used:
509
+ self._add_injection(curzone.myname, vect)
510
+ else:
511
+ logging.warning(_("Vector {} in zone {} is not used -- Ignoring it as an injection point").format(vect.myname, curzone.myname))
512
+ else:
513
+ logging.error(_("Zone {} does not contain an injection point").format(curzone.myname))
514
+
515
+ def find_rivers_upstream(self):
516
+ """ Find the upstreams of the rivers """
517
+
518
+ for curriver, curvect in self.rivers.items():
519
+ up = self._find_upstream(curvect)
520
+ self.upstreams[curriver] = (up.x, up.y)
521
+
522
+ def _find_upstream(self, curvect:vector) -> wolfvertex:
523
+ """ Find the upstream of a vector
524
+
525
+ :param curvect: The river's axis
526
+ """
527
+
528
+ vert1 = curvect.myvertices[0]
529
+ vert2 = curvect.myvertices[-1]
530
+
531
+ riv1 = self.river_system.get_nearest_nodes([vert1.x, vert1.y])[1]
532
+ riv2 = self.river_system.get_nearest_nodes([vert2.x, vert2.y])[1]
533
+
534
+ # Drained area
535
+ area1 = riv1.uparea
536
+ area2 = riv2.uparea
537
+
538
+ # Get the minimum
539
+ if area1 > area2:
540
+ return vert2
541
+ else:
542
+ return vert1
543
+
544
+ def prepare_search(self, rivers:list[str] = None):
545
+ """
546
+ Prepare the search for the hydrology model.
547
+
548
+ The order is important because the reaches will be
549
+ progressively excluded from the search for the next ones.
550
+
551
+ So, you have to start with the **main river** and then the **tributaries**.
552
+
553
+ :param rivers: The list of rivers to prepare
554
+ """
555
+
556
+ excluded = []
557
+
558
+ if rivers is None:
559
+ rivers = self.rivers.keys()
560
+ else:
561
+ for curriver in rivers:
562
+ if curriver not in self.rivers:
563
+ logging.error(f"River {curriver} not found")
564
+ return
565
+ logging.info(f"Rivers to prepare in thos order: {rivers}")
566
+
567
+ for curriver in rivers:
568
+
569
+ curvect = self.rivers[curriver]
570
+
571
+ # Récupération de la maille rivière la plus proche
572
+ dist, node_up = self.river_system.get_nearest_nodes(self.upstreams[curriver])
573
+
574
+ # Récupération de la liste des biefs en aval
575
+ downstream = self.river_system.get_downstream_reaches_excluded(node_up, excluded)
576
+
577
+ excluded += downstream
578
+
579
+ # Mis en place d'une structure de recherche rapide
580
+ nodes, kdtree = self.river_system.get_kdtree_from_reaches(downstream)
581
+
582
+ self._searching[curriver] = Searching_Context(curvect, kdtree, nodes, downstream, node_up)
583
+
584
+ def _is_global(self, col_name:str):
585
+ """ Vérifie si la colonne est un hydrogramme global """
586
+
587
+ return col_name in self.hydrographs_total.columns
588
+
589
+ def _is_partial(self, col_name:str):
590
+ """ Vérifie si la colonne est un hydrogramme partiel """
591
+
592
+ return col_name in self.hydrographs_local.columns
593
+
594
+ def _is_anthropic(self, col_name:str):
595
+ """
596
+ Vérifie si la colonne est un hydrogramme anthropique
597
+ (c'est-à-dire une colonne de l'hydrogramme total qui n'est pas un hydrogramme partiel)
598
+
599
+ """
600
+
601
+ return self._is_global(col_name) and not self._is_partial(col_name)
602
+
603
+ def _is_virtual(self, col_name:str):
604
+ """ Vérifie si la colonne est un hydrogramme virtuel """
605
+
606
+ return col_name in [virtualname for virtualname, src_name, frac, lag in self._hydrographs_virtual]
607
+
608
+ def _add_infil(self,
609
+ type_name:InjectionType,
610
+ col_name_q:str | float,
611
+ factor:float,
612
+ lag:float,
613
+ index_zone:int = None):
614
+ """
615
+ Ajoute une infiltration à la liste des infiltrations
616
+
617
+ :param type_name: nom du type d'infiltration
618
+ :param col_name: nom de la colonne de l'hydrogramme
619
+ :param factor: facteur multiplicatif
620
+ :param lag: déphasage
621
+ """
622
+
623
+ if type_name == InjectionType.GLOBAL:
624
+ if not self._is_global(col_name_q):
625
+ raise ValueError(f"Colonne {col_name_q} not found in global hydrographs")
626
+ elif type_name == InjectionType.PARTIAL:
627
+ if not self._is_partial(col_name_q):
628
+ raise ValueError(f"Colonne {col_name_q} not found in partial hydrographs")
629
+ elif type_name == InjectionType.ANTHROPOGENIC:
630
+ if not self._is_anthropic(col_name_q):
631
+ raise ValueError(f"Colonne {col_name_q} not found in anthropic hydrographs")
632
+ elif type_name == InjectionType.VIRTUAL:
633
+ if not self._is_virtual(col_name_q):
634
+ raise ValueError(f"Colonne {col_name_q} not found in virtual hydrographs")
635
+
636
+ if index_zone is None:
637
+ self.counter_zone_infil += 1
638
+ index_zone = self.counter_zone_infil
639
+
640
+ self.infiltrations.append(Scaled_Infiltration(index_zone, type_name, col_name_q, factor, lag))
641
+
642
+ return index_zone
643
+
644
+ def _add_local_injecton(self,
645
+ local_vect:vector,
646
+ type_name:InjectionType,
647
+ col_name:str,
648
+ factor:float,
649
+ lag:float):
650
+
651
+ """
652
+ Ajoute une injection locale à la liste des infiltrations
653
+ et remplissage de la matrice d'infiltration
654
+
655
+ :param local_vect: vecteur de la zone d'injection
656
+ :param type_name: nom du type d'injection
657
+ :param col_name: nom de la colonne de l'hydrogramme
658
+ :param factor: facteur multiplicatif
659
+ :param lag: déphasage
660
+
661
+ """
662
+
663
+ assert type_name in InjectionType, f"Unknown type {type_name}"
664
+
665
+ if local_vect not in self.local_infiltrations:
666
+ # Pas encore d'injection dans cette zone
667
+
668
+ self.local_infiltrations[local_vect] = self._add_infil(type_name, col_name, factor, lag)
669
+
670
+ # Mise à zéro de la sélection dans la matrice d'infiltration
671
+ self._infil_idx.SelectionData.reset()
672
+ # Sélection des mailles à l'intérieur du polygone de la zone d'injection
673
+ self._infil_idx.SelectionData.select_insidepoly(local_vect)
674
+
675
+ # Récupération des coordonnées des mailles sélectionnées
676
+ xy = np.array(self._infil_idx.SelectionData.myselection)
677
+ # Conversion des coordonnées en indices de mailles
678
+ ij = self._infil_idx.get_ij_from_xy_array(xy)
679
+
680
+ # Affectation de l'indice de la zone d'infiltration
681
+ for i,j in ij:
682
+ self._infil_idx.array[i,j] = self.local_infiltrations[local_vect]
683
+
684
+ # Vérification du nombre de mailles affectées
685
+ assert len(ij) == np.count_nonzero(self._infil_idx.array == self.local_infiltrations[local_vect]), "Bad count for {}".format(type_name)
686
+
687
+ else:
688
+ # Une injection existe déjà dans cette zone
689
+ # On ajoute une nouvelle infiltration qui sera additionnée à la précédente
690
+
691
+ self._add_infil(type_name, col_name, factor, lag, self.local_infiltrations[local_vect])
692
+
693
+
694
+ def _add_along_injection(self,
695
+ list_part:list[float, float],
696
+ type_name:InjectionType,
697
+ col_name:str,
698
+ factor:float,
699
+ lag:float):
700
+ """
701
+ Ajoute une injection le long de la rivière
702
+ et remplissage de la matrice d'infiltration
703
+
704
+ :param list_part: liste des coordonnées des points de la rivière
705
+ :param type_name: nom du type d'injection
706
+ :param col_name: nom de la colonne de l'hydrogramme
707
+ :param factor: facteur multiplicatif
708
+ :param lag: déphasage
709
+ """
710
+
711
+ idx_zone = self._add_infil(type_name, col_name, factor, lag)
712
+
713
+ for x, y in list_part:
714
+ i,j = self._infil_idx.get_ij_from_xy(x,y)
715
+ self._infil_idx.array[i,j] = idx_zone
716
+
717
+
718
+ def write_infil_array(self, dirout:Path):
719
+ """ Sauvegarde de la matrice d'infiltration """
720
+
721
+ self._infil_idx.mask_data(0)
722
+ self._infil_idx.nullvalue = 99999
723
+ self._infil_idx.set_nullvalue_in_mask()
724
+ self._infil_idx.write_all(dirout / f'infiltration.tif')
725
+
726
+ def _get_reaches_in_sub(self, subbasin:SubWatershed, rivers_names:list[str]) -> list[list[int]]:
727
+ """
728
+ Retourne une liste de listes des biefs dans le sous-bassin
729
+
730
+ :param rivers: liste des noms des rivières
731
+ :return: liste des biefs dans le sous-bassin
732
+ """
733
+
734
+ ret = []
735
+
736
+ if rivers_names[0] not in self._searching:
737
+ logging.error(f"River {rivers_names[0]} not found")
738
+ return ret
739
+
740
+ reaches1 = self._searching[rivers_names[0]].downstream_reaches
741
+
742
+ reaches_in_sub = [idx for idx in reaches1 if subbasin.is_reach_in_sub(idx)]
743
+
744
+ if len(reaches_in_sub) == 0:
745
+ logging.error(f"No reaches in subbasin for river {rivers_names[0]}")
746
+
747
+ ret.append(reaches_in_sub)
748
+
749
+ if isinstance(rivers_names[1], list):
750
+ locret = self._get_reaches_in_sub(subbasin, rivers_names[1])
751
+
752
+ for loc in locret:
753
+ ret.append(loc)
754
+ else:
755
+
756
+ if rivers_names[1] not in self._searching:
757
+ logging.error(f"River {rivers_names[1]} not found")
758
+ return ret
759
+
760
+ reaches2 = self._searching[rivers_names[1]].downstream_reaches
761
+
762
+ reaches_in_sub = [idx for idx in reaches2 if subbasin.is_reach_in_sub(idx)]
763
+ if len(reaches_in_sub) == 0:
764
+ logging.error(f"No reaches in subbasin for river {rivers_names[1]}")
765
+
766
+ ret.append(reaches_in_sub)
767
+
768
+ return ret
769
+
770
+ def _get_outlet_reaches(self, subbasin:SubWatershed, idx_reaches:list[int]) -> Node_Watershed:
771
+ """
772
+ Retourne le noeud de sortie du sous-bassin
773
+
774
+ :param reaches: liste des biefs dans le sous-bassin
775
+ :return: noeud de sortie du sous-bassin
776
+ """
777
+
778
+ down_reach = max(idx_reaches)
779
+ return subbasin.get_downstream_node_in_reach(down_reach)
780
+
781
+ def _split_subwatershed(self, subbasin:SubWatershed, river_names:list[str, list[str]]):
782
+
783
+ reaches = self._get_reaches_in_sub(subbasin, river_names)
784
+ out1 = self._get_outlet_reaches(subbasin, reaches[0])
785
+ out2 = self._get_outlet_reaches(subbasin, reaches[1])
786
+
787
+ newsub1 = self.watershed.create_virtual_subwatershed(out1, [out2])
788
+ newsub2 = self.watershed.create_virtual_subwatershed(out2)
789
+
790
+ return newsub1, newsub2
791
+
792
+ def _split_hydrographs(self, subbasin:SubWatershed | str, river_names:list[str, list[str]]):
793
+ """
794
+ Séparation de l'hydrogramme partiel en fonction
795
+ des surfaces drainées par chaque rivère
796
+
797
+ On attend au maximum 2 rivières ou 1 rivière et une liste de rivières.
798
+
799
+ Les rivières seront traitées 2 par 2 de façon récursive.
800
+
801
+ La seconde rivière et l'affluent de la première rivière.
802
+
803
+ """
804
+
805
+ if isinstance(subbasin, str):
806
+ subbasin = self.watershed.get_subwatershed(subbasin)
807
+
808
+ sub1, sub2 = self._split_subwatershed(subbasin, river_names)
809
+
810
+ fraction1 = sub1.area / subbasin.area
811
+ fraction2 = sub2.area / subbasin.area
812
+
813
+ assert fraction1 + fraction2 == 1., "Bad fractions"
814
+
815
+ self.add_virtual_hydrograph(sub1.name, subbasin.name, fraction1, 0.)
816
+ self.add_virtual_hydrograph(sub2.name, subbasin.name, fraction2, 0.)
817
+
818
+ added = []
819
+ added.append((sub1.name, river_names[0]))
820
+
821
+ if isinstance(river_names[1], list):
822
+ # Recursive call
823
+ added.extend(self._split_hydrographs(sub2, river_names[1]))
824
+ else:
825
+ added.append((sub2.name, river_names[1]))
826
+
827
+ return added
828
+
829
+ def get_locale_injection_names(self):
830
+ """ Print the names of the local injections """
831
+
832
+ return list(self.locale_injections.keys())
833
+
834
+ def get_along_injection_names(self) -> tuple[list[str], list[str]]:
835
+ """ Get the names of the along injections
836
+
837
+ :return: The names of the rivers along which the injections are made and the columns of the hydrographs
838
+ """
839
+
840
+ return list(self.lists_part.keys()), list(self.hydrographs_local.columns)
841
+
842
+ def reset_injections(self):
843
+ """ Reset the injections """
844
+
845
+ self.counter_zone_infil = 0
846
+
847
+ if self._infil_idx is not None:
848
+ self._infil_idx.array[:,:] = 0
849
+
850
+ self._hydrographs_virtual = []
851
+
852
+ self.infiltrations = []
853
+ self.local_infiltrations = {}
854
+
855
+ def spread_infiltrations(self):
856
+ """ Traite les injections """
857
+
858
+ self.reset_injections()
859
+
860
+ self.injections_locales()
861
+ self.injections_along()
862
+
863
+ self.create_hydrographs()
864
+
865
+ def injections_locales(self, couplings:list[tuple[str, str, InjectionType]] = None):
866
+ """ Ajoute les injections locales """
867
+
868
+ if couplings is None:
869
+ couplings = self._locales
870
+
871
+ if couplings is None:
872
+ logging.error(_("No local injections defined"))
873
+ return
874
+
875
+ for curinj in couplings:
876
+
877
+ vec_name, col_name, injtype = curinj
878
+
879
+ self._add_local_injecton(self.locale_injections[vec_name], injtype, col_name, 1., 0.)
880
+
881
+
882
+ def link_area2nodes(self):
883
+ """
884
+ Searching cells in dem associated to the river nodes in the hydrological model.
885
+
886
+ We use the river axis to select the cells in the dem.
887
+
888
+ Then we search the nearest river nodes in the hydrological
889
+ model.
890
+
891
+ We create local lists of cells associated to one river node.
892
+
893
+ Due to the fact that the river axis is not exactly the same
894
+ as the river nodes (not the same spatial resolution, rester vs vector),
895
+ all river nodes in the hydrological model
896
+ are not necessarely associated to cells in the dem.
897
+
898
+ """
899
+
900
+ for key_river in self.rivers:
901
+
902
+ river_axis = self._searching[key_river].river_axis
903
+ kdtree = self._searching[key_river].kdtree
904
+ nodes = self._searching[key_river].nodes
905
+
906
+ # Mise à 0 des zones sélectionnées
907
+ self._dem.SelectionData.reset()
908
+ # Sélection des mailles sur l'axe du lit mineur
909
+ self._dem.SelectionData.select_underpoly(river_axis)
910
+
911
+ # Coordonnées XY des mailles sélectionnées
912
+ xy_selected = self._dem.SelectionData.myselection
913
+
914
+ # Recherche des mailles rivières les plus proches
915
+ # dans la modélisation hydrologique
916
+ dist, nearest = kdtree.query(xy_selected, k=1)
917
+ # Récupération des noeuds correspondants aux index fournis par l'objet KDTree
918
+ nearest:list[Node_Watershed] = [nodes[i] for i in nearest]
919
+
920
+ # Surface drainée par les mailles
921
+ drained_surface = np.array([cur.uparea for cur in nearest])
922
+
923
+ # Valeurs de BV uniques
924
+ unique_area = np.unique(drained_surface)
925
+
926
+ # Création de listes contenant les mailles associé à chaque BV
927
+ list_part = {}
928
+ for cur_area in unique_area:
929
+ idx = list(np.where(drained_surface == cur_area)[0])
930
+ list_part[cur_area] = [xy_selected[i] for i in idx]
931
+
932
+ self.lists_part[key_river] = (unique_area, list_part)
933
+
934
+ def injections_along(self, along:list[str, str] = None):
935
+ """ Injections along rivers """
936
+
937
+ if along is None:
938
+ along = self._along
939
+
940
+ if along is None:
941
+ logging.error(_("No along injections defined"))
942
+ return
943
+
944
+ along = along.copy()
945
+
946
+ # ## Création de bassins virtuels afin de séparer les hydrogrammes en plusieurs rivières
947
+
948
+ # Un sous-BV peut voir son hydrogramme partiel être réparti entre un rivière ou plusieurs rivières.
949
+
950
+ # En cas de rivières multiples, la décomposition doit se faire 2 par 2:
951
+
952
+ # - rivière principale
953
+ # - rivière secondaire
954
+
955
+ # La rivière secondaire peut également être décomposée en plusieurs selon le même principe.
956
+
957
+ # **La procédure de calcul est récursive.**
958
+ to_split = [cur for cur in along if isinstance(cur[1], list)]
959
+
960
+ replace = []
961
+ for cur in to_split:
962
+ replace.extend(self._split_hydrographs(cur[0], cur[1]))
963
+
964
+ for cur in to_split:
965
+ along.remove(cur)
966
+
967
+ for cur in replace:
968
+ along.append(cur)
969
+
970
+ for cur in along:
971
+ self._injection_along(cur)
972
+
973
+ def _injection_along(self, name_subwatershed_river:tuple[str, str]):
974
+
975
+ # Nom de colonne et liste de mailles potentielles à utiliser
976
+ # pour la répartition
977
+
978
+ name_subwatershed, river = name_subwatershed_river
979
+
980
+ list_rivers, used_reaches = self.lists_part[river], self._searching[river].downstream_reaches
981
+
982
+ # sous-bassin
983
+ subbasin = self.watershed.get_subwatershed(name_subwatershed)
984
+
985
+ # Récupération des surfaces à utiliser pour l'injection en long
986
+ unique_areas, lists = list_rivers
987
+ unique_areas.sort()
988
+
989
+ # traitement des injections locales si existantes dans le ss-bv
990
+ # -------------------------------------------------------------
991
+
992
+ # Recherche des zones d'injection locales dans le sous-bassin virtuel uniquement
993
+ local_injections = subbasin.filter_zones(self._locale_injection_zones, force_virtual_if_any=True)
994
+
995
+ # surfaces traitées par les injections ponctuelles
996
+ local_areas = 0.
997
+ # liste contenant la maille de connection au réseau et la maille d'injection locale
998
+ to_remove:list[tuple[Node_Watershed, Node_Watershed, float]] = []
999
+
1000
+ for cur_locinj in local_injections:
1001
+
1002
+ # Recherche du noeud rivière le plus proche de la zone d'injection locale
1003
+ dist, node_local_injection = self.river_system.get_nearest_nodes(cur_locinj)
1004
+
1005
+ if node_local_injection.reach not in used_reaches:
1006
+ # Recherche de la maille rivière en aval
1007
+ # qui fait partie de la distribution en long
1008
+ down = self.river_system.go_downstream_until_reach_found(node_local_injection, used_reaches)
1009
+ else:
1010
+ # La zone d'injection ponctuelle est sur un des biefs à traiter
1011
+ # On cherche la première maille sur laquelle une distribution en long se fera
1012
+ down = node_local_injection
1013
+ while down is not None and down.uparea not in unique_areas:
1014
+ down = down.down
1015
+
1016
+
1017
+ # surface su sous-bassin qui sera injectée localement
1018
+ local_area = node_local_injection.uparea - subbasin.get_area_outside_sub_if_exists(node_local_injection, node_local_injection.get_up_reaches_same_sub())
1019
+
1020
+ # on retient la maille aval et la maille d'injection locale
1021
+ to_remove.append((down, node_local_injection, local_area))
1022
+
1023
+ local_areas += local_area
1024
+
1025
+ self._add_local_injecton(cur_locinj, InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL, name_subwatershed, local_area / subbasin.area, 0.)
1026
+
1027
+ # Incrément de BV à traiter pour l'injection en long
1028
+ # c-à-d la différence entre la surface drainée du ss-bv et la somme des surfaces locales
1029
+ delta_area = subbasin.area - local_areas
1030
+
1031
+ # aire drainée en aval du sous-bassin
1032
+ area_max = subbasin.outlet.uparea
1033
+
1034
+ # aire drainée à la limite amont du ss-bassin, le long de la distribution en long
1035
+ up_node = subbasin.get_up_rivernode_outside_sub(subbasin.outlet, used_reaches)
1036
+
1037
+ if up_node is None:
1038
+ starting_node = subbasin.get_list_nodes_river(min(used_reaches))[-1]
1039
+ area_min = subbasin.get_area_outside_sub_if_exists(starting_node, starting_node.get_up_reaches_same_sub())
1040
+ else:
1041
+ area_min = up_node.uparea
1042
+
1043
+ # On ne garde que la fraction utile des surfaces à traiter pour le ss-bv
1044
+ unique_areas = unique_areas[(unique_areas >= area_min) & (unique_areas<=area_max)]
1045
+
1046
+ if unique_areas[0] != area_min:
1047
+ unique_areas = np.insert(unique_areas, 0, area_min)
1048
+
1049
+ frac_sum=0.
1050
+
1051
+ def area_to_remove(node:Node_Watershed) -> float:
1052
+
1053
+ uparea = 0.
1054
+
1055
+ # injections locales
1056
+ for node_down, node_inj, loc_area in to_remove:
1057
+ if node == node_down:
1058
+ if node_inj.reach in used_reaches:
1059
+ uparea += loc_area
1060
+ else:
1061
+ uparea += node_inj.uparea
1062
+
1063
+ for node_up in node.upriver:
1064
+ if node_up.sub == node.sub:
1065
+ if not subbasin.is_in_rivers(node_up):
1066
+ uparea += node_up.uparea
1067
+
1068
+ return uparea
1069
+
1070
+ for idx in range(1,len(unique_areas)-1):
1071
+
1072
+ up_node = subbasin.get_river_nodes_from_upareas(unique_areas[idx], unique_areas[idx])[0]
1073
+ delta_loc = unique_areas[idx] - unique_areas[idx-1] - area_to_remove(up_node)
1074
+
1075
+ fraction_loc = delta_loc / delta_area
1076
+
1077
+ if fraction_loc > 0.:
1078
+
1079
+ self._add_along_injection(lists[unique_areas[idx]],
1080
+ InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1081
+ name_subwatershed,
1082
+ fraction_loc,
1083
+ lag =0.)
1084
+
1085
+ frac_sum += fraction_loc
1086
+ elif fraction_loc < 0.:
1087
+ logging.error(f"Bad fraction {fraction_loc} " + name_subwatershed)
1088
+
1089
+ delta_loc = area_max - unique_areas[-2] - area_to_remove(subbasin.outlet)
1090
+
1091
+ fraction_loc = delta_loc / delta_area
1092
+
1093
+ self._add_along_injection(lists[unique_areas[-1]],
1094
+ InjectionType.PARTIAL if not subbasin._is_virtual else InjectionType.VIRTUAL,
1095
+ name_subwatershed,
1096
+ fraction_loc,
1097
+ lag =0.)
1098
+
1099
+ frac_sum += fraction_loc
1100
+
1101
+ if frac_sum > 1.001 or frac_sum < 0.999:
1102
+ logging.error(f"Bad sum of fractions {frac_sum} " + name_subwatershed)
1103
+
1104
+
1105
+ def create_hydrographs(self):
1106
+ """ Création des hydrogrammes
1107
+
1108
+ Les étapes précédentes ont ajouté à la liste "infiltrations" les éléments suivants:
1109
+
1110
+ - l'index de la zone d'infiltration (1-based)
1111
+ - l'hydrogramme de référence
1112
+ - la fecteur pondérateur
1113
+ - le temps de déphasage
1114
+
1115
+ Une zone peut contenir plusieurs apports.
1116
+
1117
+ Il faut donc parcourir l'ensemble des zones et sommer les contributions.
1118
+
1119
+ Le fichier final est ordonné comme la matrice d'infiltration.
1120
+
1121
+ 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)
1122
+ """
1123
+
1124
+ dt = self.hydrographs_total.index[1] - self.hydrographs_total.index[0]
1125
+
1126
+ # Création des hydrogrammes virtuels
1127
+ # Un hydrograme virtuel ne peut être créé que par des hydrogrammes partiels ou virtuels
1128
+ df_virtual = pd.DataFrame()
1129
+ df_virtual.index.name = 'Time'
1130
+ df_virtual.index = self.hydrographs_total.index
1131
+
1132
+ for cur_vrt in self._hydrographs_virtual:
1133
+ name, src_hydrograph_name, factor, lag = cur_vrt
1134
+
1135
+ decal = int(lag / dt)
1136
+
1137
+ if src_hydrograph_name in self.hydrographs_local.columns:
1138
+ df_virtual[name] = self.hydrographs_local.shift(decal, fill_value=0.)[src_hydrograph_name] * factor
1139
+
1140
+ elif src_hydrograph_name in df_virtual.columns:
1141
+ df_virtual[name] = df_virtual.shift(decal, fill_value=0.)[src_hydrograph_name] * factor
1142
+
1143
+ df_2d_dict = {}
1144
+ df_2d_dict['Time'] = self.hydrographs_total.index
1145
+
1146
+ nb_infil = max([cur.index for cur in self.infiltrations])
1147
+
1148
+ counter = np.asarray([cur.index for cur in self.infiltrations])
1149
+ counter = [np.count_nonzero(counter == cur) for cur in range(1, nb_infil+1)]
1150
+
1151
+ # FIXME : pas optimisé
1152
+ for i in range(1, nb_infil+1):
1153
+ # Bouclage sur les zones d'infiltation
1154
+
1155
+ loc_count = 0
1156
+
1157
+ for cur_infil in self.infiltrations:
1158
+ # Bouclage sur les infiltrations
1159
+ # En effet, plusieurs hydrogrammes peuvent être associés à une même zone
1160
+
1161
+ idx, type_name, col_name, factor, lag = cur_infil.index, cur_infil.type, cur_infil.colref, cur_infil.factor, cur_infil.lagtime
1162
+
1163
+ if idx == i:
1164
+
1165
+ decal = int(lag / dt)
1166
+ loc_count += 1
1167
+
1168
+ if type_name == InjectionType.GLOBAL:
1169
+
1170
+ if loc_count == 1:
1171
+ df_2d_dict[idx] = self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1172
+ else:
1173
+ df_2d_dict[idx] += self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1174
+
1175
+ elif type_name == InjectionType.PARTIAL:
1176
+
1177
+ if loc_count == 1:
1178
+ df_2d_dict[idx] = self.hydrographs_local.shift(decal, fill_value = 0.)[col_name] * factor
1179
+ else:
1180
+ df_2d_dict[idx] += self.hydrographs_local.shift(decal, fill_value = 0.)[col_name] * factor
1181
+
1182
+ elif type_name == InjectionType.ANTHROPOGENIC:
1183
+
1184
+ if loc_count == 1:
1185
+ df_2d_dict[idx] = self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1186
+ else:
1187
+ df_2d_dict[idx] += self.hydrographs_total.shift(decal, fill_value = 0.)[col_name] * factor
1188
+
1189
+ elif type_name == InjectionType.VIRTUAL:
1190
+
1191
+ if loc_count == 1:
1192
+ df_2d_dict[idx] = df_virtual.shift(decal, fill_value = 0.)[col_name] * factor
1193
+ else:
1194
+ df_2d_dict[idx] += df_virtual.shift(decal, fill_value = 0.)[col_name] * factor
1195
+
1196
+ elif type_name == InjectionType.CONSTANT:
1197
+
1198
+ if loc_count == 1:
1199
+ df_2d_dict[idx] = np.asarray([col_name]*len(df_2d_dict[('Time')])) * factor
1200
+ else:
1201
+ df_2d_dict[idx] += col_name * factor
1202
+
1203
+ else:
1204
+ logging.error(f"Unknown type {type_name}")
1205
+
1206
+ if loc_count != counter[i-1]:
1207
+ logging.error(f"Bad count for {i}")
1208
+
1209
+ self.df_2d = pd.DataFrame(df_2d_dict)
1210
+ self.df_2d.set_index('Time', inplace=True)
1211
+
1212
+
1213
+ def save_hydrographs(self, dirout:Path, name:str):
1214
+ """ Write the hydrographs"""
1215
+
1216
+ with open(dirout / (name + f'_infiltration_zones.txt'), 'w') as f:
1217
+ f.write("Zone\tType\tColonne\tFacteur\tLag\n")
1218
+ for cur in self.infiltrations:
1219
+ idx, type_name, col_name, factor, lag = cur.index, cur.type.value, cur.colref, cur.factor, cur.lagtime
1220
+ f.write(f"{idx}\t{type_name}\t{col_name}\t{factor}\t{lag}\n")
1221
+
1222
+ if self.df_2d is None:
1223
+ logging.error("No hydrographs created")
1224
+ return
1225
+
1226
+ self.df_2d.to_csv(dirout / name, sep='\t', decimal='.', encoding='latin1')