tilupy 2.0.0__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.
tilupy/utils.py ADDED
@@ -0,0 +1,656 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+
8
+ import shapely.geometry as geom
9
+ import shapely.ops
10
+
11
+ import tilupy.read
12
+
13
+
14
+ def CSI(pred: np.ndarray, obs: np.ndarray) -> float:
15
+ """Compute the Critical Success Index (CSI).
16
+
17
+ Measure the fraction of observed and/or predicted events that were correctly predicted.
18
+
19
+ Parameters
20
+ ----------
21
+ pred : numpy.ndarray
22
+ Array of predicted binary values (e.g., 1 for event predicted, 0 otherwise).
23
+ Shape and type must match :data:`obs`.
24
+ obs : numpy.ndarray
25
+ Array of observed binary values (e.g., 1 for event observed, 0 otherwise).
26
+ Shape and type must match :data:`pred`.
27
+
28
+ Returns
29
+ -------
30
+ float
31
+ The Critical Success Index (CSI), a score between 0 and 1.
32
+ A score of 1 indicates perfect prediction, while 0 indicates no skill.
33
+ """
34
+ ipred = pred > 0
35
+ iobs = obs > 0
36
+
37
+ TP = np.sum(ipred * iobs)
38
+ FP = np.sum(ipred * ~iobs)
39
+ FN = np.sum(~ipred * iobs)
40
+
41
+ return TP / (TP + FP + FN)
42
+
43
+
44
+ def diff_runout(x_contour: np.ndarray,
45
+ y_contour: np.ndarray,
46
+ point_ref: tuple[float, float],
47
+ section: np.ndarray | shapely.geometry.LineString = None,
48
+ orientation: str = "W-E"
49
+ ) -> float:
50
+ """Compute runout distance difference between a reference point and its projection on a contour,
51
+ optionally along a specified section line.
52
+
53
+ This function calculates the distance from a reference point to a contour (e.g., a polygon boundary),
54
+ or the distance along a section line (e.g., a transect) between the reference point and its intersection
55
+ with the contour. The section line can be oriented in four cardinal directions.
56
+
57
+ Parameters
58
+ ----------
59
+ x_contour : numpy.ndarray
60
+ Array of x-coordinates defining the contour.
61
+ y_contour : numpy.ndarray
62
+ Array of y-coordinates defining the contour. Must be the same length as :data:`x_contour`.
63
+ point_ref : tuple[float, float]
64
+ Reference point coordinates (x, y) from which the distance is calculated.
65
+ section : numpy.ndarray or shapely.geometry.LineString, optional
66
+ Coordinates of the section line as an array of shape (N, 2) or a Shapely LineString.
67
+ If None, the function returns the Euclidean distance from :data:`point_ref` to the contour.
68
+ By default None.
69
+ orientation : str, optional
70
+ Orientation of the section line. Must be one of:
71
+
72
+ - "W-E" (West-East, default)
73
+ - "E-W" (East-West)
74
+ - "S-N" (South-North)
75
+ - "N-S" (North-South)
76
+
77
+ This determines how the intersection point is selected if multiple intersections exist.
78
+ By default "W-E".
79
+
80
+ Returns
81
+ -------
82
+ float
83
+ If `section` is None: Euclidean distance from :data:`point_ref` to the contour.
84
+ If `section` is provided: Distance along the section line between :data:`point_ref` and the contour intersection.
85
+ The distance is signed, depending on the projection direction.
86
+ """
87
+ npts = len(x_contour)
88
+ contour = geom.LineString(
89
+ [(x_contour[i], y_contour[i]) for i in range(npts)]
90
+ )
91
+ point = geom.Point(point_ref)
92
+ if section is None:
93
+ return point.distance(contour)
94
+ elif isinstance(section, np.ndarray):
95
+ section = geom.LineString(section)
96
+
97
+ assert isinstance(section, geom.LineString)
98
+ section = revert_line(section, orientation)
99
+ intersections = section.intersection(contour)
100
+ if isinstance(intersections, geom.MultiPoint):
101
+ intersections = geom.LineString(intersections.geoms)
102
+ intersections = np.array(intersections.coords)
103
+ if orientation == "W-E":
104
+ i = np.argmax(intersections[:, 0])
105
+ if orientation == "E-W":
106
+ i = np.argmin(intersections[:, 0])
107
+ if orientation == "S-N":
108
+ i = np.argmax(intersections[:, 1])
109
+ if orientation == "N-S":
110
+ i = np.argmin(intersections[:, 1])
111
+ intersection = geom.Point(intersections[i, :])
112
+
113
+ #######
114
+ # plt.figure()
115
+ # cont = np.array(contour.coords)
116
+ # sec = np.array(section.coords)
117
+ # inter = np.array(intersection.coords)
118
+ # pt = np.array(point.coords)
119
+ # plt.plot(cont[:,0], cont[:,1],
120
+ # sec[:,0], sec[:,1],
121
+ # pt[:,0], pt[:,1], 'o',
122
+ # inter[:,0], inter[:,1],'x')
123
+ #######
124
+
125
+ return section.project(intersection) - section.project(point)
126
+
127
+
128
+ def revert_line(line: shapely.geometry.LineString,
129
+ orientation: str = "W-E"
130
+ ) -> shapely.geometry.LineString:
131
+ """Revert a line geometry if its orientation does not match the specified direction.
132
+
133
+ This function checks the orientation of the input line (based on the coordinates of its first and last points)
134
+ and reverses it if necessary to ensure it matches the specified cardinal direction.
135
+ The line is reversed in-place if the initial orientation is opposite to the specified one.
136
+
137
+ Parameters
138
+ ----------
139
+ line : shapely.geometry.LineString
140
+ Input line geometry to be checked and potentially reverted.
141
+ orientation : str, optional
142
+ Desired cardinal direction for the line. Must be one of:
143
+
144
+ - "W-E" (West-East, default): The line should start at the west end (smaller x-coordinate).
145
+ - "E-W" (East-West): The line should start at the east end (larger x-coordinate).
146
+ - "S-N" (South-North): The line should start at the south end (smaller y-coordinate).
147
+ - "N-S" (North-South): The line should start at the north end (larger y-coordinate).
148
+ By default "W-E".
149
+
150
+ Returns
151
+ -------
152
+ shapely.geometry.LineString
153
+ The input line, potentially reverted to match the specified orientation.
154
+ If the line already matches the orientation, it is returned unchanged.
155
+ """
156
+ pt_init = line.coords[0]
157
+ pt_end = line.coords[-1]
158
+ if orientation == "W-E":
159
+ if pt_init[0] > pt_end[0]:
160
+ line = shapely.ops.substring(line, 1, 0, normalized=True)
161
+ elif orientation == "E-W":
162
+ if pt_init[0] < pt_end[0]:
163
+ line = shapely.ops.substring(line, 1, 0, normalized=True)
164
+ if orientation == "S-N":
165
+ if pt_init[1] > pt_end[1]:
166
+ line = shapely.ops.substring(line, 1, 0, normalized=True)
167
+ elif orientation == "N-S":
168
+ if pt_init[1] < pt_end[1]:
169
+ line = shapely.ops.substring(line, 1, 0, normalized=True)
170
+
171
+ return line
172
+
173
+
174
+ def get_contour(x: np.ndarray,
175
+ y: np.ndarray,
176
+ z: np.ndarray,
177
+ zlevels: list[float],
178
+ indstep: int = 1,
179
+ maxdist: float = 30,
180
+ closed_contour: bool = True
181
+ ) -> tuple[dict[float, np.ndarray],
182
+ dict[float, np.ndarray]]:
183
+ """Extract contour lines from a 2D grid of values at specified levels.
184
+
185
+ This function computes contour lines for a given 2D field :data:`z` defined on the grid :data:`(x, y)`,
186
+ at the specified levels :data:`zlevels`. It can optionally ensure that contours are closed by padding
187
+ the input arrays, and filter out contours that are not closed within a maximum distance threshold.
188
+
189
+ Parameters
190
+ ----------
191
+ x : numpy.ndarray
192
+ 1D array of x-coordinates defining the grid.
193
+ y : numpy.ndarray
194
+ 1D array of y-coordinates defining the grid.
195
+ z : numpy.ndarray
196
+ 2D array of values defined on the grid :data:`(x, y)`.
197
+ zlevels : list[float]
198
+ List of contour levels at which to extract the contours.
199
+ indstep : int, optional
200
+ Step used to subsample the contour points, by default 1 (no subsampling).
201
+ maxdist : float, optional
202
+ Maximum allowed distance between the first and last points of a contour line.
203
+ If the distance exceeds this value and `closed_contour` is False, the contour is discarded,
204
+ by default 30.
205
+ closed_contour : bool, optional
206
+ If True, the input arrays are padded to ensure closed contours, by default True.
207
+
208
+ Returns
209
+ -------
210
+ tuple[dict[float, np.ndarray], dict[float, np.ndarray]]
211
+ xcontour : dict
212
+ Maps each contour level to an array of x-coordinates of the contour line.
213
+ ycontour : dict
214
+ Maps each contour level to an array of y-coordinates of the contour line.
215
+ If a contour is discarded due to `maxdist`, the corresponding value is `None`.
216
+ """
217
+ # Add sup_value at the border of the array, to make sure contour
218
+ # lines are closed
219
+ if closed_contour:
220
+ z2 = z.copy()
221
+ ni = z2.shape[0]
222
+ nj = z2.shape[1]
223
+ z2 = np.vstack([np.zeros((1, nj)), z2, np.zeros((1, nj))])
224
+ z2 = np.hstack([np.zeros((ni + 2, 1)), z2, np.zeros((ni + 2, 1))])
225
+ dxi = x[1] - x[0]
226
+ dxf = x[-1] - x[-2]
227
+ dyi = y[1] - y[0]
228
+ dyf = y[-1] - y[-2]
229
+ x2 = np.insert(np.append(x, x[-1] + dxf), 0, x[0] - dxi)
230
+ y2 = np.insert(np.append(y, y[-1] + dyf), 0, y[0] - dyi)
231
+ else:
232
+ x2, y2, z2 = x, y, z
233
+
234
+ backend = plt.get_backend()
235
+ plt.switch_backend("Agg")
236
+ fig = plt.figure()
237
+ ax = plt.gca()
238
+ cs = ax.contour(x2, y2, np.flip(z2, 0), zlevels)
239
+ nn1 = 1
240
+ v1 = np.zeros((1, 2))
241
+ xcontour = {}
242
+ ycontour = {}
243
+ for indlevel in range(len(zlevels)):
244
+ levels = cs.allsegs[indlevel]
245
+ for p in levels:
246
+ if p.shape[0] > nn1:
247
+ v1 = p
248
+ nn1 = p.shape[0]
249
+ xc = [v1[::indstep, 0]]
250
+ yc = [v1[::indstep, 1]]
251
+ if maxdist is not None and not closed_contour:
252
+ ddx = np.abs(v1[0, 0] - v1[-1, 0])
253
+ ddy = np.abs(v1[0, 1] - v1[-1, 1])
254
+ dd = np.sqrt(ddx**2 + ddy**2)
255
+ if dd > maxdist:
256
+ xc[0] = None
257
+ yc[0] = None
258
+ xcontour[zlevels[indlevel]] = xc[0]
259
+ ycontour[zlevels[indlevel]] = yc[0]
260
+ plt.close(fig)
261
+ plt.switch_backend(backend)
262
+ return xcontour, ycontour
263
+
264
+
265
+ def get_profile(data: tilupy.read.TemporalResults2D | tilupy.read.StaticResults2D,
266
+ extraction_mode: str = "axis",
267
+ data_threshold: float = 1e-3,
268
+ **extraction_params,
269
+ ) -> tuple[tilupy.read.TemporalResults1D | tilupy.read.StaticResults1D,
270
+ float | tuple[np.ndarray] | np.ndarray]:
271
+ """Extract profile with different modes and options.
272
+
273
+ Parameters
274
+ ----------
275
+ data : tilupy.read.TemporalResults2D or tilupy.read.StaticResults2D
276
+ Data to extract the profile from.
277
+ extraction_mode : str, optional
278
+ Method to extract profiles:
279
+
280
+ - "axis": Extracts a profile along an axis.
281
+ - "coordinates": Extracts a profile along specified coordinates.
282
+ - "shapefile": Extracts a profile along a shapefile (polylines).
283
+
284
+ Be default "axis".
285
+
286
+ data_threshold : float, optional
287
+ Minimum value to consider as part of the profile, by default 1e-3.
288
+
289
+ extraction_params : dict, optional
290
+ Different parameters to be entered depending on the extraction method chosen:
291
+
292
+ - If :data:`extraction_mode == "axis"`:
293
+
294
+ - :data:`axis`: str, optional
295
+ Axis where to extract the profile ['X', 'Y'], by default 'Y'.
296
+ - :data:`profile_position`: float, optional
297
+ Position where to extract the profile. If None choose the median.
298
+ By default None.
299
+ Must be read: profile in :data:`axis = profile_position m`.
300
+
301
+ - If :data:`extraction_mode == "coordinates"`:
302
+
303
+ - :data:`xcoord`: numpy.ndarray, optional
304
+ X coordinates of the profile, by default :attr:`_x`.
305
+ - :data:`ycoord`: numpy.ndarray, optional
306
+ Y coordinates of the profile, by default :data:`[0., 0., ...]`.
307
+
308
+ - If :data:`extraction_mode == "shapefile"`:
309
+
310
+ - :data:`path`: str
311
+ Path to the shapefile.
312
+ - :data:`x_origin`: float, optional
313
+ Value of the X coordinate of the origin (top-left corner) in the shapefile's coordinate system, by default 0.0 (EPSG:2154).
314
+ - :data:`y_origin`: float, optional
315
+ Value of the y coordinate of the origin (top-left corner) in the shapefile's coordinate system, by default :data:`_y[-1]` (EPSG:2154).
316
+ - :data:`x_pixsize`: float, optional
317
+ Size of a pixel along the X coordinate in the shapefile's coordinate system, by default :data:`_x[1] - _x[0]` (EPSG:2154).
318
+ - :data:`y_pixsize`: float, optional
319
+ Size of a pixel along the Y coordinate in the shapefile's coordinate system, by default :data:`_y[1] - _y[0]` (EPSG:2154).
320
+ - :data:`step`: float, optional
321
+ Spatial step between profile points, by default 10.0.
322
+
323
+ By default None
324
+
325
+ Returns
326
+ -------
327
+ tuple[tilupy.read.TemporalResults1D or tilupy.read.StaticResults1D, float or tuple[np.ndarray] or np.ndarray]
328
+ tilupy.read.TemporalResults1D or tilupy.read.StaticResults1D
329
+ Extracted profiles.
330
+ float or tuple[np.ndarray] or numpy.ndarray
331
+ Specific output depending on :data:`extraction_mode`:
332
+
333
+ - If :data:`extraction_mode == "axis"`: float
334
+ Position of the profile.
335
+ - If :data:`extraction_mode == "coordinates"`: tuple[numpy.ndarray]
336
+ X coordinates, Y coordinates and distance values.
337
+ - If :data:`extraction_mode == "shapefile"`: numpy.ndarray
338
+ Distance values.
339
+
340
+ Raises
341
+ ------
342
+ ValueError
343
+ If :data:`extraction_mode == "axis"` and if invalid :data:`axis`.
344
+ ValueError
345
+ If :data:`extraction_mode == "axis"` and if invalid format for :data:`profile_position`.
346
+ ValueError
347
+ If :data:`extraction_mode == "axis"` and if no value position found in axis.
348
+ ValueError
349
+ If :data:`extraction_mode == "coordinates"` and if invalid format for :data:`xcoord` or :data:`ycoord`.
350
+ ValueError
351
+ If :data:`extraction_mode == "coordinates"` and if invalid dimension for :data:`xcoord` or :data:`ycoord`.
352
+ ValueError
353
+ If :data:`extraction_mode == "coordinates"` and if :data:`xcoord` and :data:`ycoord` doesn't have same size.
354
+ ValueError
355
+ If :data:`extraction_mode == "shapefile"` and if no :data`path` is given.
356
+ ValueError
357
+ If :data:`extraction_mode == "shapefile"` and if invalid format for :data:`x_origin`, :data:`y_origin`, :data:`x_pixsize`, :data:`y_pixsize` or :data:`step`.
358
+ TypeError
359
+ If :data:`extraction_mode == "shapefile"` and if invalid geometry for the shapefile.
360
+ ValueError
361
+ If :data:`extraction_mode == "shapefile"` and if no linestring found in the shapefile.
362
+ ValueError
363
+ If invalid :data:`extraction_mode`.
364
+ """
365
+ if not isinstance(data, tilupy.read.TemporalResults2D) and not isinstance(data, tilupy.read.StaticResults2D):
366
+ raise ValueError("Can only extract profile from 2D data.")
367
+
368
+ y_coord, x_coord = data.y, data.x
369
+ y_size, x_size, = len(y_coord), len(x_coord)
370
+ data_field = data.d.copy()
371
+
372
+ # Apply mask on data
373
+ data_field[data_field <= data_threshold] = 0
374
+
375
+ extraction_params = {} if extraction_params is None else extraction_params
376
+
377
+ if extraction_mode == "axis":
378
+ # Create specific params if not given
379
+ if "axis" not in extraction_params:
380
+ extraction_params["axis"] = 'Y'
381
+ if "profile_position" not in extraction_params:
382
+ extraction_params["profile_position"] = None
383
+
384
+ # Check errors
385
+ if extraction_params["axis"] not in ['x', 'X', 'y', 'Y']:
386
+ raise ValueError("Invalid axis: 'X' or 'Y'.")
387
+ extraction_params["axis"] = extraction_params["axis"].upper()
388
+
389
+ # Depending on "profile_position" type, choose median or position value
390
+ if extraction_params["profile_position"] is None:
391
+ if extraction_params["axis"] == 'X':
392
+ extraction_params["profile_index"] = x_size//2
393
+ closest_value=x_coord[extraction_params["profile_index"]]
394
+ else:
395
+ extraction_params["profile_index"] = y_size//2
396
+ closest_value=y_coord[extraction_params["profile_index"]]
397
+
398
+ elif isinstance(extraction_params["profile_position"], float) or isinstance(extraction_params["profile_position"], int):
399
+ coord_val = extraction_params["profile_position"]
400
+ x_index, y_index = None, None
401
+
402
+ if extraction_params["axis"] == 'X':
403
+ if not isinstance(x_coord, np.ndarray):
404
+ x_coord = np.array(x_coord)
405
+
406
+ x_index = np.argmin(np.abs(x_coord - coord_val))
407
+ closest_value = x_coord[x_index]
408
+
409
+ if x_index is None:
410
+ raise ValueError(f"Find no values, must be: {x_coord}")
411
+ extraction_params["profile_index"] = x_index
412
+ else:
413
+ if not isinstance(y_coord, np.ndarray):
414
+ y_coord = np.array(y_coord)
415
+
416
+ y_index = np.argmin(np.abs(y_coord - coord_val))
417
+ closest_value = y_coord[y_index]
418
+
419
+ if y_index is None:
420
+ raise ValueError(f"Find no values, must be: {y_coord}")
421
+ extraction_params["profile_index"] = y_index
422
+
423
+ else:
424
+ raise ValueError("Invalid format for 'profile_position'. Must be None or float position.")
425
+
426
+ # Return profiles
427
+ if extraction_params["axis"] == 'X':
428
+ if isinstance(data, tilupy.read.TemporalResults2D):
429
+ return (tilupy.read.TemporalResults1D(name=data.name,
430
+ d=data_field[:, extraction_params["profile_index"], :],
431
+ t=data.t,
432
+ coords=data.y,
433
+ coords_name='y',
434
+ notation=data.notation),
435
+ closest_value)
436
+ else:
437
+ return (tilupy.read.StaticResults1D(name=data.name,
438
+ d=data_field[:, extraction_params["profile_index"]],
439
+ coords=data.y,
440
+ coords_name='y',
441
+ notation=data.notation),
442
+ closest_value)
443
+ else:
444
+ if isinstance(data, tilupy.read.TemporalResults2D):
445
+ return (tilupy.read.TemporalResults1D(name=data.name,
446
+ d=data_field[extraction_params["profile_index"], :, :],
447
+ t=data.t,
448
+ coords=data.x,
449
+ coords_name='x',
450
+ notation=data.notation),
451
+ closest_value)
452
+ else:
453
+ return (tilupy.read.StaticResults1D(name=data.name,
454
+ d=data_field[extraction_params["profile_index"], :],
455
+ coords=data.x,
456
+ coords_name='x',
457
+ notation=data.notation),
458
+ closest_value)
459
+
460
+ elif extraction_mode == "coordinates":
461
+ if "xcoord" not in extraction_params:
462
+ extraction_params["xcoord"] = x_coord[:]
463
+ if "ycoord" not in extraction_params:
464
+ extraction_params["ycoord"] = [0] * len(x_coord)
465
+
466
+ # Check errors
467
+ if not isinstance(extraction_params["xcoord"], np.ndarray):
468
+ if isinstance(extraction_params["xcoord"], list):
469
+ extraction_params["xcoord"] = np.array(extraction_params["xcoord"])
470
+ else:
471
+ raise ValueError("Invalid format for 'xcoord'. Must be a numpy array.")
472
+ if not isinstance(extraction_params["ycoord"], np.ndarray):
473
+ if isinstance(extraction_params["ycoord"], list):
474
+ extraction_params["ycoord"] = np.array(extraction_params["ycoord"])
475
+ else:
476
+ raise ValueError("Invalid format for 'ycoord'. Must be a numpy array.")
477
+
478
+ if extraction_params["xcoord"].ndim != 1:
479
+ raise ValueError("Invild dimension. 'xcoord' must be a 1d array.")
480
+ if extraction_params["ycoord"].ndim != 1:
481
+ raise ValueError("Invild dimension. 'ycoord' must be a 1d array.")
482
+
483
+ if len(extraction_params["xcoord"]) != len(extraction_params["ycoord"]):
484
+ raise ValueError(f"'xcoord' and 'ycoord' must have same size: ({len(extraction_params['xcoord'])}, {len(extraction_params['ycoord'])})")
485
+
486
+ # Extract index from nearest value of xcoord and ycoord
487
+ x_distances = np.abs(x_coord[None, :] - extraction_params["xcoord"][:, None])
488
+ y_distances = np.abs(y_coord[None, :] - extraction_params["ycoord"][:, None])
489
+
490
+ x_indexes = np.argmin(x_distances, axis=1)
491
+ y_indexes = np.argmin(y_distances, axis=1)
492
+
493
+ # Compute distance
494
+ x_values = x_coord[x_indexes]
495
+ y_values = y_coord[y_indexes]
496
+
497
+ dx = np.diff(x_values)
498
+ dy = np.diff(y_values)
499
+
500
+ distance = np.sqrt(dx**2 + dy**2)
501
+ distance = np.concatenate(([0], np.cumsum(distance)))
502
+
503
+ # Return profile
504
+ if isinstance(data, tilupy.read.TemporalResults2D):
505
+ return (tilupy.read.TemporalResults1D(name=data.name,
506
+ d=data_field[y_indexes, x_indexes, :],
507
+ t=data.t,
508
+ coords=distance,
509
+ coords_name='d'),
510
+ (x_values,
511
+ y_values,
512
+ distance))
513
+ else:
514
+ return (tilupy.read.StaticResults1D(name=data.name,
515
+ d=data_field[y_indexes, x_indexes],
516
+ coords=distance,
517
+ coords_name='d'),
518
+ (x_values,
519
+ y_values,
520
+ distance))
521
+
522
+ elif extraction_mode == "shapefile" :
523
+ if "path" not in extraction_params:
524
+ extraction_params["path"] = None
525
+ if "x_origin" not in extraction_params:
526
+ extraction_params["x_origin"] = 0
527
+ if "y_origin" not in extraction_params:
528
+ extraction_params["y_origin"] = y_coord[-1]
529
+ if "x_pixsize" not in extraction_params:
530
+ extraction_params["x_pixsize"] = x_coord[1] - x_coord[0]
531
+ if "y_pixsize" not in extraction_params:
532
+ extraction_params["y_pixsize"] = y_coord[1] - y_coord[0]
533
+ if "step" not in extraction_params:
534
+ extraction_params["step"] = 10
535
+
536
+ # Check errors
537
+ if extraction_params["path"] is None:
538
+ raise ValueError("No path to the shape file given.")
539
+
540
+ if not isinstance(extraction_params["x_origin"], float) and not isinstance(extraction_params["x_origin"], int):
541
+ raise ValueError("'x_origin' must be float.")
542
+ if not isinstance(extraction_params["y_origin"], float) and not isinstance(extraction_params["y_origin"], int):
543
+ raise ValueError("'y_origin' must be float.")
544
+ if not isinstance(extraction_params["x_pixsize"], float) and not isinstance(extraction_params["x_pixsize"], int):
545
+ raise ValueError("'x_pixsize' must be float.")
546
+ if not isinstance(extraction_params["y_pixsize"], float) and not isinstance(extraction_params["y_pixsize"], int):
547
+ raise ValueError("'y_pixsize' must be float.")
548
+
549
+ if not isinstance(extraction_params["step"], float) and not isinstance(extraction_params["step"], int):
550
+ raise ValueError("'step' must be float.")
551
+
552
+ # Import specific module and define extraction function
553
+ from shapely.geometry import LineString, MultiLineString
554
+ from shapely.ops import linemerge
555
+ import geopandas as gpd
556
+ from affine import Affine
557
+
558
+ def extract_lines_from_shp_file(shapefile_path):
559
+ """Extract LineString objects from a shapefile.
560
+ """
561
+ gdf = gpd.read_file(shapefile_path)
562
+ lines = []
563
+ for geom in gdf.geometry:
564
+ if isinstance(geom, LineString):
565
+ lines.append(geom)
566
+ elif isinstance(geom, MultiLineString):
567
+ lines.extend(list(geom))
568
+ else:
569
+ raise TypeError(f"Invalid geometry: {type(geom)}")
570
+ if not lines:
571
+ raise ValueError("No Linestring found.")
572
+ return lines
573
+
574
+ lines = extract_lines_from_shp_file(extraction_params["path"])
575
+
576
+ # If multiple lines, merge them together
577
+ merged = linemerge(lines)
578
+ if isinstance(merged, LineString):
579
+ profile_line = merged
580
+ else:
581
+ profile_line = LineString([pt for line in merged for pt in line.coords])
582
+
583
+ # Construct the affine transformation
584
+ transform = (Affine.translation(extraction_params["x_origin"],
585
+ extraction_params["y_origin"])
586
+ * Affine.scale(extraction_params["x_pixsize"],
587
+ -extraction_params["y_pixsize"]))
588
+ inv = ~transform # invert : (x, y) -> (row, col)
589
+
590
+ distances = np.arange(0,
591
+ profile_line.length,
592
+ extraction_params["step"])
593
+ points = [profile_line.interpolate(d) for d in distances]
594
+
595
+ # Conversion coordinates -> indexes
596
+ rowcols = [inv * (pt.x, pt.y) for pt in points]
597
+ rowcols = [(int(round(r)), int(round(c))) for c, r in rowcols]
598
+
599
+ # Extract values
600
+ all_values = []
601
+ for t in range(len(data.t)):
602
+ values = []
603
+ valid_distances = []
604
+ for d, (r, c) in zip(distances, rowcols):
605
+ if 0 <= r < data_field.shape[0] and 0 <= c < data_field.shape[1]:
606
+ values.append(data_field[r, c, t])
607
+ valid_distances.append(d)
608
+ all_values.append(values)
609
+
610
+ all_values = np.array(all_values)
611
+ valid_distances = np.array(valid_distances)
612
+
613
+ # Return profile
614
+ if isinstance(data, tilupy.read.TemporalResults2D):
615
+ return (tilupy.read.TemporalResults1D(name=data.name,
616
+ d=all_values.T,
617
+ t=data.t,
618
+ coords=valid_distances,
619
+ coords_name="d",
620
+ notation=data.notation),
621
+ valid_distances)
622
+ else:
623
+ return (tilupy.read.StaticResults1D(name=data.name,
624
+ d=all_values.T,
625
+ coords=valid_distances,
626
+ coords_name="d",
627
+ notation=data.notation),
628
+ valid_distances)
629
+ else :
630
+ raise ValueError("Invalid 'extraction_mode': 'axis', 'coordinates' or 'shapefile'.")
631
+
632
+
633
+ def format_path_linux(path: str) -> str:
634
+ """
635
+ Change a Windows-type path to a path formatted for Linux. \\ are changed
636
+ to /, and partitions like "C:" are changed to "/mnt/c/"
637
+
638
+ Parameters
639
+ ----------
640
+ path : string
641
+ String with the path to be modified.
642
+
643
+ Returns
644
+ -------
645
+ path2 : string
646
+ Formatted path.
647
+
648
+ """
649
+ if path[1] == ":":
650
+ path2 = "/mnt/{:s}/".format(path[0].lower()) + path[2:]
651
+ else:
652
+ path2 = path
653
+ path2 = path2.replace("\\", "/")
654
+ if " " in path2:
655
+ path2 = '"' + path2 + '"'
656
+ return path2