wolfhece 2.1.126__py3-none-any.whl → 2.1.127__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.
@@ -894,6 +894,10 @@ if :\n \
894
894
  self.myprops.addparam('Image','Attached image','',Type_Param.File, '', whichdict='Default')
895
895
  self.myprops.addparam('Image','To show',False,Type_Param.Logical,'',whichdict='Default')
896
896
 
897
+ self.myprops.addparam('Geometry','Length 2D',99999.,Type_Param.Float,'',whichdict='Default')
898
+ self.myprops.addparam('Geometry','Length 3D',99999.,Type_Param.Float,'',whichdict='Default')
899
+ self.myprops.addparam('Geometry','Surface',99999.,Type_Param.Float,'',whichdict='Default')
900
+
897
901
  def destroyprop(self):
898
902
  """
899
903
  Nullify the properties UI
@@ -1021,6 +1025,12 @@ if :\n \
1021
1025
 
1022
1026
  self.myprops[('Move','Delta X')] = 0.
1023
1027
  self.myprops[('Move','Delta Y')] = 0.
1028
+
1029
+ self.parent.update_lengths()
1030
+ self.myprops[( 'Geometry','Length 2D')] = self.parent.length2D
1031
+ self.myprops[( 'Geometry','Length 3D')] = self.parent.length3D
1032
+ self.myprops[( 'Geometry','Surface')] = self.parent.area
1033
+
1024
1034
  self.myprops.Populate()
1025
1035
  class vector:
1026
1036
  """
@@ -6322,10 +6332,20 @@ class Zones(wx.Frame, Element_To_Draw):
6322
6332
  boxright.Add(self.interpxyz,0,wx.EXPAND)
6323
6333
  boxright.Add(self.sascending,0,wx.EXPAND)
6324
6334
 
6335
+
6336
+ sizer_values_surface = wx.BoxSizer(wx.HORIZONTAL)
6325
6337
  self.butgetval = wx.Button(self,label=_('Get values'))
6326
6338
  self.butgetval.SetToolTip(_("Get values of the attached/active array (not working with 2D results) on each vertex of the active vector and update the editor"))
6327
6339
  self.butgetval.Bind(wx.EVT_BUTTON,self.Ongetvalues)
6328
- boxright.Add(self.butgetval,0,wx.EXPAND)
6340
+
6341
+ self.btn_surface = wx.Button(self,label=_('Surface'))
6342
+ self.btn_surface.SetToolTip(_("Compute the surface of the active vector/polygon"))
6343
+ self.btn_surface.Bind(wx.EVT_BUTTON,self.Onsurface)
6344
+
6345
+ sizer_values_surface.Add(self.butgetval,1,wx.EXPAND)
6346
+ sizer_values_surface.Add(self.btn_surface,1,wx.EXPAND)
6347
+
6348
+ boxright.Add(sizer_values_surface,0,wx.EXPAND)
6329
6349
 
6330
6350
  self.butgetvallinked = wx.Button(self,label=_('Get values (all)'))
6331
6351
  self.butgetvallinked.SetToolTip(_("Get values of all the visible arrays and 2D results on each vertex of the active vector \n\n Create a new zone containing the results"))
@@ -6893,6 +6913,18 @@ class Zones(wx.Frame, Element_To_Draw):
6893
6913
  except:
6894
6914
  raise Warning(_('Not supported in the current parent -- see PyVertexVectors in Ongetvalues function'))
6895
6915
 
6916
+ def Onsurface(self, e:wx.MouseEvent):
6917
+ """
6918
+ Calcul de la surface du vecteur actif
6919
+ """
6920
+
6921
+ if self.verify_activevec():
6922
+ return
6923
+
6924
+ dlg = wx.MessageDialog(None, _('The surface of the active vector is : {} m²'.format(self.active_vector.surface)), style = wx.OK)
6925
+ dlg.ShowModal()
6926
+ dlg.Destroy()
6927
+
6896
6928
  def Ongetvalueslinked(self, e:wx.MouseEvent):
6897
6929
  """
6898
6930
  Récupération des valeurs sous toutes les matrices liées pour le vecteur actif
wolfhece/apps/version.py CHANGED
@@ -5,7 +5,7 @@ class WolfVersion():
5
5
 
6
6
  self.major = 2
7
7
  self.minor = 1
8
- self.patch = 126
8
+ self.patch = 127
9
9
 
10
10
  def __str__(self):
11
11
 
wolfhece/eikonal.py ADDED
@@ -0,0 +1,495 @@
1
+ import numpy as np
2
+ import heapq
3
+ import multiprocessing as mp
4
+ import scipy.ndimage
5
+ import logging
6
+ from numba import njit
7
+ from tqdm import tqdm
8
+
9
+ @njit
10
+ def _evaluate_distance_and_data_first_order_iso(i, j,
11
+ fixed:np.ndarray, where_compute:np.ndarray,
12
+ dx_mesh:float, dy_mesh:float,
13
+ times:np.ndarray,
14
+ base_data:np.ndarray, test_data:np.ndarray,
15
+ speed:float = 1.0) -> tuple[float, float]:
16
+ """
17
+ Evaluate the time and data using a first order isotropic method.
18
+
19
+ :param i: (int): The row index.
20
+ :param j: (int): The column index.
21
+ :param fixed: (2D numpy array): A boolean array where True indicates fixed points.
22
+ :param where_compute: (2D numpy array): A boolean array where True indicates cells to be included in computation.
23
+ :param dx_mesh: (float): The mesh size in x direction.
24
+ :param dy_mesh: (float): The mesh size in y direction.
25
+ :param times: (2D numpy array): The time function.
26
+ :param base_data: (2D numpy array): The base data that will propagate.
27
+ :param test_data: (2D numpy array): The test data that will be used to validate propagation.
28
+ :param speed: (float): The isotropic propagation speed.
29
+
30
+ :return: tuple[float, float]: The time and data.
31
+ """
32
+
33
+ # search for fixed neighbors
34
+ fixed_neighbors = [(i + di, j + dj) for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)] if fixed[i + di, j + dj] and not where_compute[i + di, j + dj] and test_data[i, j] < base_data[i + di, j + dj]]
35
+
36
+ if len(fixed_neighbors) == 0:
37
+ return np.inf, test_data[i, j]
38
+
39
+ a_data = a_time = np.float64(len(fixed_neighbors))
40
+ b_time = np.sum(np.asarray([np.abs(times[cur]) for cur in fixed_neighbors]), dtype=np.float64)
41
+ c_time = np.sum(np.asarray([times[cur] ** 2 for cur in fixed_neighbors]), dtype= np.float64)
42
+
43
+ b_data = np.sum(np.asarray([base_data[cur] for cur in fixed_neighbors]))
44
+
45
+ # Résolution d'une équation du second degré pour trouver la distance
46
+ b_time = -2.0 * b_time
47
+ # l'hypothèse implicite dx = dy est faite ici
48
+ c_time = c_time - dx_mesh*dy_mesh * speed**2.0
49
+
50
+ if b_time != 0. and c_time!= 0.:
51
+ Realisant = abs(b_time*b_time-4*a_time*c_time)
52
+ new_time = (-b_time+np.sqrt(Realisant)) / (2.0*a_time)
53
+
54
+ elif c_time == 0.0 and b_time != 0.:
55
+ new_time = -b_time/a_time
56
+
57
+ elif b_time == 0.:
58
+ new_time = np.sqrt(-c_time/a_time)
59
+
60
+ data = b_data/a_data
61
+
62
+ if data < test_data[i,j]:
63
+ return np.inf, test_data[i,j]
64
+
65
+ return new_time, data
66
+
67
+ def __solve_eikonal_with_data(sources:list[list[int,int]],
68
+ where_compute:np.ndarray,
69
+ base_data:np.ndarray,
70
+ test_data:np.ndarray,
71
+ speed:np.ndarray,
72
+ dx_mesh:float, dy_mesh:float) -> np.ndarray:
73
+ """ Solve the Eikonal equation using the Fast Marching Method (FMM).
74
+
75
+ Jit version of the function. The next one is the non-jit version which uses this.
76
+ """
77
+
78
+ # store the fixed points
79
+ # - fix sources points
80
+ # - fix every first element of the heap after pop
81
+ fixed = np.zeros(speed.shape, dtype = np.bool8)
82
+
83
+ # fix the border
84
+ fixed[:, 0] = True
85
+ fixed[:, -1] = True
86
+ fixed[0, :] = True
87
+ fixed[-1, :] = True
88
+
89
+ time = np.ones(base_data.shape) * np.inf
90
+ time[:,0] = 0.
91
+ time[:,-1] = 0.
92
+ time[0,:] = 0.
93
+ time[-1,:] = 0.
94
+
95
+ heap = [(0., sources[0])]
96
+ for source in sources:
97
+ time[source[0], source[1]] = 0
98
+ fixed[source[0], source[1]] = True
99
+ heapq.heappush(heap, (0., source))
100
+
101
+ while heap:
102
+ t, (i, j) = heapq.heappop(heap)
103
+
104
+ fixed[i,j] = True
105
+ where_compute[i,j] = False # as we are sure that this point is fixed
106
+
107
+ # Add neighbors to the heap if not already added
108
+ for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
109
+ new_i, new_j = i + di, j + dj
110
+
111
+ if fixed[new_i, new_j] or not where_compute[new_i, new_j]:
112
+ continue
113
+
114
+ new_t, new_data = _evaluate_distance_and_data_first_order_iso(new_i, new_j,
115
+ fixed,
116
+ where_compute,
117
+ dx_mesh, dy_mesh,
118
+ time,
119
+ base_data,
120
+ test_data,
121
+ speed[new_i, new_j])
122
+
123
+ if new_t < time[new_i, new_j]:
124
+ time[new_i, new_j] = new_t
125
+ base_data[new_i, new_j] = new_data
126
+ heapq.heappush(heap, (new_t, (new_i, new_j)))
127
+
128
+ return time
129
+
130
+ def _solve_eikonal_with_data(sources:list[tuple[int,int]],
131
+ where_compute:np.ndarray=None,
132
+ base_data:np.ndarray=None,
133
+ test_data:np.ndarray=None,
134
+ speed:np.ndarray = None,
135
+ dx_mesh:float = 1.0, dy_mesh:float = 1.0) -> np.ndarray:
136
+ """
137
+ Solve the Eikonal equation using the Fast Marching Method (FMM).
138
+
139
+ :param sources: (list of tuples): The coordinates of the source points.
140
+ :param where_compute: (2D numpy array, optional): A boolean array where True indicates cells to be included in computation.
141
+ :param base_data: (2D numpy array, optional): The base data that will propagate.
142
+ :param test_data: (2D numpy array, optional): The test data that will be used to validate propagation.
143
+ :param speed: (2D numpy array): The speed function.
144
+ :param dx_mesh: (float, optional): The mesh size in x direction.
145
+ :param dy_mesh: (float, optional): The mesh size in y direction.
146
+
147
+ :return: 2D numpy array The solution to the Eikonal equation.
148
+ """
149
+
150
+ rows, cols = where_compute.shape
151
+
152
+ assert base_data.shape == where_compute.shape
153
+ assert test_data.shape == where_compute.shape
154
+
155
+ if len(sources) == 0:
156
+ raise ValueError("No sources provided.")
157
+
158
+ if speed is None:
159
+ speed = np.ones((rows, cols))
160
+
161
+ # Ensure sources are tuple
162
+ #
163
+ # FIXME : We force a global tupleof tuples because list of tuple can cause problems with Numba
164
+ # https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-reflection-for-list-and-set-types
165
+ sources = list(list(source) for source in sources)
166
+
167
+ return __solve_eikonal_with_data(sources, where_compute, base_data, test_data, speed, dx_mesh, dy_mesh)
168
+
169
+ def _extract_patch_slices_with_margin(shape, labels, margin= 2):
170
+ """ Extract the slices of the patches with a margin around the labels.
171
+
172
+ :param shape: (tuple): The shape of the array.
173
+ :param labels: (2D numpy array): The labels of the patches.
174
+ :param margin: (int, optional): The margin around the labels.
175
+ """
176
+
177
+ slices = scipy.ndimage.find_objects(labels)
178
+ patches_slices = []
179
+
180
+ for s in slices:
181
+ sl1 = slice(max(0, s[0].start - margin), min(shape[0], s[0].stop + margin))
182
+ sl2 = slice(max(0, s[1].start - margin), min(shape[1], s[1].stop + margin))
183
+ patches_slices.append((sl1, sl2))
184
+
185
+ return patches_slices
186
+
187
+ def _process_submatrix(args):
188
+ """ Function to process a submatrix in a multiprocess. """
189
+
190
+ id_label, speed, where_compute, base_data, test_data, labels, dx, dy, NoData = args
191
+
192
+ # Ignore the border
193
+ labels[:,0] = -1
194
+ labels[:,-1] = -1
195
+ labels[0,:] = -1
196
+ labels[-1,:] = -1
197
+ sources = np.argwhere(np.logical_and(labels == 0, base_data != NoData))
198
+ return (args, _solve_eikonal_with_data(sources, where_compute, base_data, test_data, speed, dx, dy))
199
+
200
+ def _solve_eikonal_with_value_on_submatrices(where_compute:np.ndarray,
201
+ base_data:np.ndarray,
202
+ test_data:np.ndarray = None,
203
+ speed:np.ndarray = None,
204
+ multiprocess:bool = False,
205
+ dx:float = 1., dy:float = 1.,
206
+ ignore_last_patches:int = 1,
207
+ NoDataValue:float = 0.) -> np.ndarray:
208
+ """ Propagate data inside the mask using the Fast Marching Method (FMM).
209
+
210
+ "base_data" will be updated with the new values.
211
+
212
+ :param where_compute: (2D numpy array): A boolean array where True indicates cells to be included in computation.
213
+ :param base_data: (2D numpy array): The base data that will propagate.
214
+ :param test_data: (2D numpy array, optional): The test data that will be used to validate propagation (we used upstream data only if base_data > test_data).
215
+ :param speed: (2D numpy array, optional): The isotropic propagation speed.
216
+ :param multiprocess: (bool, optional): Whether to use multiprocessing.
217
+ :param dx: (float, optional): The mesh size in x direction.
218
+ :param dy: (float, optional): The mesh size in y direction.
219
+
220
+ :return: 2D numpy array The solution to the Eikonal equation.
221
+ """
222
+
223
+ # Labelling the patches
224
+ labels, numfeatures = scipy.ndimage.label(where_compute)
225
+
226
+ if ignore_last_patches > 0:
227
+ # counts the number of cells in each patch
228
+ sizes = np.bincount(labels.ravel())
229
+
230
+ # get the indices of the patches sorted by size
231
+ indices = np.argsort(sizes[1:])[::-1]
232
+
233
+ # ignore the last patches
234
+ for idx in indices[:ignore_last_patches]:
235
+ where_compute[labels == idx+1] = 0 # idx +1 because np.argsort starts at 1, so "indices" are shifted by 1
236
+
237
+ # relabel the patches
238
+ labels, numfeatures = scipy.ndimage.label(where_compute)
239
+
240
+ logging.info(f"Ignoring {ignore_last_patches} last patches.")
241
+
242
+ logging.info(f"Number of patches: {numfeatures}")
243
+
244
+ if numfeatures == 0:
245
+ logging.warning("No patch to compute.")
246
+ return np.zeros_like(where_compute)
247
+
248
+ if speed is None:
249
+ logging.debug("Speed not provided. Using default value of 1.")
250
+ speed = np.ones_like(where_compute)
251
+
252
+ if test_data is None:
253
+ logging.debug("Test data not provided. Using -inf.")
254
+ test_data = np.full_like(base_data, -np.inf)
255
+
256
+ # Extract the slices of the patches with a margin of 2 cells around the labels.
257
+ # 2 cells are added to avoid test during computation.
258
+ # The external border will be ignored.
259
+ patch_slices = _extract_patch_slices_with_margin(where_compute.shape, labels)
260
+
261
+ # Prepare the submatrices to be processed
262
+ subs = [(idx+1, speed[cur], where_compute[cur], base_data[cur], test_data[cur], labels[cur].copy(), dx, dy, NoDataValue) for idx, cur in enumerate(patch_slices)]
263
+
264
+ if multiprocess:
265
+ # In multiprocess mode, the base_data is updated in a local copy of the submatrix.
266
+ # We need to merge the results at the end.
267
+ with mp.Pool(processes=max(min(mp.cpu_count(), numfeatures),1)) as pool:
268
+ results = pool.map(_process_submatrix, subs)
269
+
270
+ time = np.zeros_like(where_compute)
271
+ for slice, (sub, result) in zip(patch_slices, results):
272
+ useful_result = sub[3] != NoDataValue
273
+ base_data[slice][useful_result] = sub[3][useful_result]
274
+ time[slice][useful_result] = result[useful_result]
275
+
276
+ else:
277
+ # In single process mode, the base_data is updated in place.
278
+ # We do not need to merge the results but only the time.
279
+ # results = [_process_submatrix(sub) for sub in subs]
280
+
281
+ results = []
282
+ for sub in tqdm(subs):
283
+ results.append(_process_submatrix(sub))
284
+
285
+ time = np.zeros_like(where_compute)
286
+ for slice, (sub, result) in zip(patch_slices, results):
287
+ useful_result = result != NoDataValue
288
+ time[slice][useful_result] = result[useful_result]
289
+
290
+ return time
291
+
292
+
293
+ def count_holes(mask:np.ndarray = None):
294
+ """ Count the number of holes in the mask. """
295
+
296
+ labels, numfeatures = scipy.ndimage.label(mask)
297
+
298
+ return numfeatures
299
+
300
+ def inpaint_array(data:np.ndarray | np.ma.MaskedArray,
301
+ where_compute:np.ndarray,
302
+ test:np.ndarray,
303
+ ignore_last_patches:int = 1,
304
+ inplace:bool = True,
305
+ dx:float = 1., dy:float = 1.,
306
+ NoDataValue:float = 0.,
307
+ multiprocess:bool = True) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
308
+ """ Inpaint the array using the Fast Marching Method (FMM).
309
+
310
+ Main idea:
311
+ - We have a 2D array "data" that we want to inpaint.
312
+ - We have a 2D array "test" that will be used to validate the inpainting.
313
+ - We have a 2D array "where_compute" that indicates where to compute the inpainting.
314
+ - We will use the FMM to propagate the data inside the mask.
315
+ - We will update the data only if the new value is greater than the test data.
316
+ - (We can ignore n greatest patches to avoid computation in some large areas.)
317
+
318
+ :param data: (2D numpy array): The water level to inpaint.
319
+ :param mask: (2D numpy array): The simulation's Digital Elevation Model (DEM).
320
+ :param test: (2D numpy array, optional): The digital terrain model (DTM).
321
+ :param ignore_last_patches: (int, optional): The number of last patches to ignore.
322
+ :param inplace: (bool, optional): Whether to update the water_level in place.
323
+ :param dx: (float, optional): The mesh size in x direction.
324
+ :param dy: (float, optional): The mesh size in y direction.
325
+ :param NoDataValue: (float, optional): The NoDataValue, used if mask is not explicitly provided (mask atribute or water_level as a Numpy MaskedArray). Default is 0.
326
+ :param multiprocess: (bool, optional): Whether to use multiprocessing.
327
+ """
328
+ if inplace:
329
+ if isinstance(data, np.ma.MaskedArray):
330
+ base_data = data.data
331
+ else:
332
+ base_data = data
333
+ else:
334
+ if isinstance(data, np.ma.MaskedArray):
335
+ base_data = data.data.copy()
336
+ else:
337
+ base_data = data.copy()
338
+
339
+ assert where_compute.shape == base_data.shape
340
+ assert test.shape == base_data.shape
341
+
342
+ time = _solve_eikonal_with_value_on_submatrices(where_compute,
343
+ base_data,
344
+ test,
345
+ speed=None,
346
+ dx=dx, dy=dy,
347
+ ignore_last_patches=ignore_last_patches,
348
+ NoDataValue=NoDataValue,
349
+ multiprocess=multiprocess)
350
+
351
+ if inplace:
352
+ if isinstance(data, np.ma.MaskedArray):
353
+ data.mask[:,:] = base_data == NoDataValue
354
+
355
+ extra = np.ma.masked_array(base_data - test, mask=data.mask.copy())
356
+ extra.data[extra.data <= 0.] = NoDataValue
357
+ extra.mask[extra.data == NoDataValue] = True
358
+ else:
359
+ extra = base_data - test
360
+ extra[extra <= 0.] = NoDataValue
361
+ else:
362
+ if isinstance(data, np.ma.MaskedArray):
363
+ data = np.ma.masked_array(base_data, mask=base_data == NoDataValue)
364
+
365
+ extra = np.ma.masked_array(base_data - test, mask=data.mask)
366
+ extra.data[extra.data <= 0.] = NoDataValue
367
+ extra.mask[extra.data == NoDataValue] = True
368
+ else:
369
+ extra = base_data - test
370
+ extra[extra <= 0.] = NoDataValue
371
+
372
+ return time, data, extra
373
+
374
+ def inpaint_waterlevel(water_level:np.ndarray | np.ma.MaskedArray,
375
+ dem:np.ndarray,
376
+ dtm:np.ndarray,
377
+ ignore_last_patches:int = 1,
378
+ inplace:bool = True,
379
+ dx:float = 1., dy:float = 1.,
380
+ NoDataValue:float = 0.,
381
+ multiprocess:bool = True) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
382
+ """ Inpaint the water level using the Fast Marching Method (FMM). Similar to the HOLES.EXE Fortran program.
383
+
384
+ Assumptions:
385
+ - The simulations are in a steady state.
386
+ - The flood extent is limited by:
387
+ - natural topography (where DEM == DTM)
388
+ - buildings or blocks of buildings
389
+ - protective structures
390
+
391
+ The goal is to propagate the free surface elevations into the buildings.
392
+
393
+ We calculate the difference between the DEM (including buildings and walls) and the DTM to identify where the buildings are.
394
+
395
+ Specifically:
396
+ - if it is natural topography, the differences will be zero or almost zero
397
+ - if it is a building or anthropogenic structure, the differences are significant
398
+
399
+ We set the elevations of the cells where the difference is zero to a value unreachable by the flood.
400
+ Thus, only buildings in contact with the flood will be affected and filled.
401
+
402
+ HOLES.EXE vs Python code:
403
+ - In "holes.exe", we must provide "in", "mask", and "dem" files:
404
+ - "in" is the water level
405
+ - "dem" is the digital terrain model (DTM) associated with the simulation's topo-bathymetry (not the topo-bathymetry itself)
406
+ - "mask" is the DTM where the buildings are identified and a large value (above the maximum water level) if the cell is not inside a building.
407
+ - The patches are identified by the water level "0.0" in the "in" file.
408
+ - FMM is used and new water levels are calculated and retained if greater than the "mask" value.
409
+ - The DTM is only used in the final part when evaluating the new water depths (Z-DTM).
410
+
411
+ - In Python, we must provide the water level, the DEM, and the DTM:
412
+ - The patches will be identified by the buildings array (filtered DEM - DTM).
413
+ - FMM is used and new water levels are calculated and retained if greater than the DTM value.
414
+ - FMM is only propagated in the cells where "buildings = True".
415
+
416
+ - Final results must be the same even if the algorithm is a little bit different.
417
+ - Fortran code demands the "mask" file to be provided (pre-computed/modified by the user), but in Python, we calculate it automatically from the DEM and DTM.
418
+ - We can use "inpaint_array" to be more flexible... by manually providing a "where_compute" and a "test" arrays
419
+
420
+ :param water_level: (2D numpy array): The water level to inpaint.
421
+ :param dem: (2D numpy array): The simulation's Digital Elevation Model (DEM).
422
+ :param dtm: (2D numpy array, optional): The digital terrain model (DTM).
423
+ :param ignore_last_patches: (int, optional): The number of last patches to ignore.
424
+ :param inplace: (bool, optional): Whether to update the water_level in place.
425
+ :param dx: (float, optional): The mesh size in x direction.
426
+ :param dy: (float, optional): The mesh size in y direction.
427
+ :param NoDataValue: (float, optional): The NoDataValue, used if mask is not explicitly provided (mask attribute or water_level as a Numpy MaskedArray). Default is 0.
428
+ :param multiprocess: (bool, optional): Whether to use multiprocessing.
429
+ """
430
+ if inplace:
431
+ if isinstance(water_level, np.ma.MaskedArray):
432
+ base_data = water_level.data
433
+ else:
434
+ base_data = water_level
435
+ else:
436
+ if isinstance(water_level, np.ma.MaskedArray):
437
+ base_data = water_level.data.copy()
438
+ else:
439
+ base_data = water_level.copy()
440
+
441
+ assert dem.shape == base_data.shape
442
+ assert dtm.shape == base_data.shape
443
+
444
+ # Create the mask where we can fill the water level
445
+
446
+ # first we identify the buildings by the difference between the DEM and the DTM
447
+ buildings = dem - dtm
448
+ # If DTM is above DEM, we set the value to 0
449
+ if np.any(buildings < 0.) > 0:
450
+ logging.warning("Some cells in the DTM are above the DEM.")
451
+ logging.info("Setting these values to 0.")
452
+ buildings[buildings < 0.] = 0.
453
+
454
+ # If DTM is below DEM, we set the value to 1
455
+ buildings[buildings > 0.] = 1.
456
+
457
+ # We interplate only if building cells are not already in the water_level
458
+ comp = np.logical_and(buildings == 1., base_data != NoDataValue)
459
+ if np.any(comp):
460
+ logging.warning("Some building cells are already flooded.")
461
+ logging.info("Ignoring these cells in the interpolation.")
462
+
463
+ buildings = np.logical_and(buildings, base_data == NoDataValue)
464
+
465
+ time = _solve_eikonal_with_value_on_submatrices(buildings,
466
+ base_data,
467
+ dtm,
468
+ speed=None,
469
+ dx=dx, dy=dy,
470
+ ignore_last_patches=ignore_last_patches,
471
+ NoDataValue=NoDataValue,
472
+ multiprocess=multiprocess)
473
+
474
+ if inplace:
475
+ if isinstance(water_level, np.ma.MaskedArray):
476
+ water_level.mask[:,:] = base_data == NoDataValue
477
+
478
+ water_height = np.ma.masked_array(base_data - dtm, mask=water_level.mask.copy())
479
+ water_height.data[water_height.data <= 0.] = NoDataValue
480
+ water_height.mask[water_height.data == NoDataValue] = True
481
+ else:
482
+ water_height = base_data - dtm
483
+ water_height[water_height <= 0.] = NoDataValue
484
+ else:
485
+ if isinstance(water_level, np.ma.MaskedArray):
486
+ water_level = np.ma.masked_array(base_data, mask=base_data == NoDataValue)
487
+
488
+ water_height = np.ma.masked_array(base_data - dtm, mask=water_level.mask)
489
+ water_height.data[water_height.data <= 0.] = NoDataValue
490
+ water_height.mask[water_height.data == NoDataValue] = True
491
+ else:
492
+ water_height = base_data - dtm
493
+ water_height[water_height <= 0.] = NoDataValue
494
+
495
+ return time, water_level, water_height
@@ -571,6 +571,9 @@ class xyz_laz_grids():
571
571
  return ret
572
572
 
573
573
  def find_files_in_bounds(self, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
574
+ """ Find all files in bounds
575
+
576
+ :param bounds: [[xmin,xmax], [ymin,ymax]]"""
574
577
 
575
578
  ret = [(cur.mydir, cur.find_files_in_bounds(bounds)) for cur in self.grids]
576
579
  ret = [cur for cur in ret if len(cur[1])>0]
@@ -583,6 +586,11 @@ class xyz_laz_grids():
583
586
  return ret
584
587
 
585
588
  def copy_files_in_bounds(self, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]], dirout:str):
589
+ """ Copy files in bounds to directory
590
+
591
+ :param bounds: [[xmin,xmax], [ymin,ymax]]
592
+ :param dirout: output directory
593
+ """
586
594
 
587
595
  import shutil
588
596
 
@@ -601,11 +609,28 @@ class xyz_laz_grids():
601
609
  shutil.copy(join(curdir,'gridinfo.txt'), locdir / 'gridinfo.txt')
602
610
 
603
611
  def read_dir(self, dir_grids):
612
+ """ Read all grids in directory and subdirectories """
613
+
614
+ import asyncio
615
+
604
616
  dirs = listdir(dir_grids)
605
617
 
606
- for curdir in dirs:
607
- if Path(curdir).is_dir():
608
- self.grids.append(xyz_laz_grid(join(dir_grids,curdir)))
618
+ logging.info(_('Reading grids information -- {} grids'.format(len(dirs))))
619
+
620
+ def init_grid(curdir):
621
+ if (Path(dir_grids) / Path(curdir)).is_dir():
622
+ return xyz_laz_grid(join(dir_grids, curdir))
623
+
624
+ return None
625
+
626
+ async def init_grid_async(curdir):
627
+ return init_grid(curdir)
628
+
629
+ loop = asyncio.get_event_loop()
630
+ tasks = [init_grid_async(curdir) for curdir in dirs]
631
+ self.grids = list(filter(None, loop.run_until_complete(asyncio.gather(*tasks))))
632
+
633
+ logging.info(_('Grids initialized'))
609
634
 
610
635
  def scan_around(self, xy:Union[LineString,list[list[float], list[float]]], length_buffer=5.):
611
636
  """