pyreclaim 0.1.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.
- pyreclaim-0.1.0.dist-info/METADATA +803 -0
- pyreclaim-0.1.0.dist-info/RECORD +27 -0
- pyreclaim-0.1.0.dist-info/WHEEL +5 -0
- pyreclaim-0.1.0.dist-info/licenses/LICENSE +674 -0
- pyreclaim-0.1.0.dist-info/top_level.txt +1 -0
- reclaim/__init__.py +1 -0
- reclaim/derived_features/__init__.py +1 -0
- reclaim/derived_features/feature_engineering_and_transformation.py +75 -0
- reclaim/dynamic_features/__init__.py +1 -0
- reclaim/dynamic_features/catchment_dynamic.py +103 -0
- reclaim/dynamic_features/reservoir_dynamic.py +148 -0
- reclaim/dynamic_features/utils/__init__.py +1 -0
- reclaim/dynamic_features/utils/catchment_meteorology.py +0 -0
- reclaim/dynamic_features/utils/inflow_outflow.py +95 -0
- reclaim/dynamic_features/utils/rainfall.py +49 -0
- reclaim/dynamic_features/utils/statistical_metrics.py +190 -0
- reclaim/dynamic_features/utils/ts_aggregate.py +63 -0
- reclaim/generate_features.py +141 -0
- reclaim/reclaim.py +503 -0
- reclaim/static_features/__init__.py +1 -0
- reclaim/static_features/catchment_static.py +127 -0
- reclaim/static_features/reservoir_static.py +97 -0
- reclaim/static_features/utils/__init__.py +1 -0
- reclaim/static_features/utils/aec_shape.py +101 -0
- reclaim/static_features/utils/area_perimeter.py +36 -0
- reclaim/static_features/utils/catchment_agreggate.py +147 -0
- reclaim/static_features/utils/flow_length.py +455 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import networkx as nx # NetworkX library import
|
|
5
|
+
|
|
6
|
+
from shapely.geometry import shape, Point, LineString, Polygon, GeometryCollection, MultiLineString, MultiPolygon
|
|
7
|
+
from shapely.ops import transform, split, linemerge, unary_union
|
|
8
|
+
from shapely.geometry.polygon import orient
|
|
9
|
+
from shapely.strtree import STRtree
|
|
10
|
+
from shapely.validation import explain_validity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_optimal_resolution(geometry, min_resolution=0.0001, max_resolution=0.1, complexity_weight=0.38):
|
|
14
|
+
"""
|
|
15
|
+
Estimate an optimal grid resolution for pathfinding within a given geometry.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
- geometry (Polygon or MultiPolygon): The geometry to analyze.
|
|
19
|
+
- min_resolution (float): Minimum allowable resolution.
|
|
20
|
+
- max_resolution (float): Maximum allowable resolution.
|
|
21
|
+
- complexity_weight (float): Weight that determines how much the resolution adapts to complexity.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
- float: Suggested resolution.
|
|
25
|
+
"""
|
|
26
|
+
# Handle MultiPolygon by taking the union
|
|
27
|
+
if isinstance(geometry, MultiPolygon):
|
|
28
|
+
geometry = geometry.buffer(0) # Clean union
|
|
29
|
+
|
|
30
|
+
area = geometry.area
|
|
31
|
+
perimeter = geometry.length
|
|
32
|
+
|
|
33
|
+
if area == 0:
|
|
34
|
+
raise ValueError("Geometry has zero area.")
|
|
35
|
+
|
|
36
|
+
avg_scale = np.sqrt(area)
|
|
37
|
+
complexity = perimeter / avg_scale # Higher means more detail
|
|
38
|
+
|
|
39
|
+
# Base resolution proportional to avg scale
|
|
40
|
+
base_resolution = avg_scale * complexity_weight / complexity
|
|
41
|
+
|
|
42
|
+
# Clamp to user-defined bounds
|
|
43
|
+
resolution = max(min_resolution, min(base_resolution, max_resolution))
|
|
44
|
+
|
|
45
|
+
return round(resolution, 5)
|
|
46
|
+
|
|
47
|
+
def get_largest_polygon(geometry):
|
|
48
|
+
"""Return the largest polygon from a Polygon or MultiPolygon"""
|
|
49
|
+
if isinstance(geometry, Polygon):
|
|
50
|
+
return geometry
|
|
51
|
+
elif isinstance(geometry, MultiPolygon):
|
|
52
|
+
return max(geometry.geoms, key=lambda p: p.area)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("Input must be a Polygon or MultiPolygon.")
|
|
55
|
+
|
|
56
|
+
def extract_valid_lines(geometry):
|
|
57
|
+
"""Ensure that only valid LineString segments are returned, regardless of input geometry type."""
|
|
58
|
+
if isinstance(geometry, LineString):
|
|
59
|
+
return [geometry]
|
|
60
|
+
elif isinstance(geometry, (GeometryCollection, MultiLineString)):
|
|
61
|
+
valid_lines = []
|
|
62
|
+
for geom in geometry.geoms:
|
|
63
|
+
if isinstance(geom, LineString):
|
|
64
|
+
valid_lines.append(geom)
|
|
65
|
+
return valid_lines
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def shape_index(polygon):
|
|
69
|
+
return (polygon.length ** 2) / polygon.area
|
|
70
|
+
|
|
71
|
+
def compute_initial_tolerance(geometry, fraction=0.005):
|
|
72
|
+
"""
|
|
73
|
+
Compute a tolerance based on the size of the geometry.
|
|
74
|
+
Smaller fraction = higher fidelity.
|
|
75
|
+
"""
|
|
76
|
+
minx, miny, maxx, maxy = geometry.bounds
|
|
77
|
+
diag = ((maxx - minx)**2 + (maxy - miny)**2)**0.5
|
|
78
|
+
return diag * fraction # 0.5% of diagonal as tolerance
|
|
79
|
+
|
|
80
|
+
def clean_narrow_necks(geometry, buffer_fraction=0.002):
|
|
81
|
+
"""
|
|
82
|
+
Buffer in and out to remove narrow slivers or necks without overly changing geometry.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
- geometry: Polygon or MultiPolygon
|
|
86
|
+
- buffer_fraction: Fraction of geometry diagonal to use as buffer width.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
- cleaned geometry
|
|
90
|
+
"""
|
|
91
|
+
minx, miny, maxx, maxy = geometry.bounds
|
|
92
|
+
diag = ((maxx - minx)**2 + (maxy - miny)**2)**0.5
|
|
93
|
+
buffer_dist = diag * buffer_fraction
|
|
94
|
+
|
|
95
|
+
# Inward then outward buffer to remove narrow parts
|
|
96
|
+
cleaned = geometry.buffer(-buffer_dist).buffer(buffer_dist)
|
|
97
|
+
|
|
98
|
+
# Preserve original geometry if buffer killed it
|
|
99
|
+
if cleaned.is_empty or cleaned.area == 0:
|
|
100
|
+
return geometry
|
|
101
|
+
|
|
102
|
+
# If result is MultiPolygon, merge it
|
|
103
|
+
return unary_union(cleaned)
|
|
104
|
+
|
|
105
|
+
def filter_narrow_removed_parts(removed, min_area=0.000001, min_aspect_ratio=2):
|
|
106
|
+
"""
|
|
107
|
+
Filter out noisy edge slivers by area and shape.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
- removed: a Polygon or MultiPolygon
|
|
111
|
+
- min_area: minimum area to be considered significant
|
|
112
|
+
- min_aspect_ratio: minimum elongation to count as a narrow feature
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
- MultiPolygon of retained significant narrow parts
|
|
116
|
+
"""
|
|
117
|
+
filtered_parts = []
|
|
118
|
+
|
|
119
|
+
if isinstance(removed, Polygon):
|
|
120
|
+
candidates = [removed]
|
|
121
|
+
elif isinstance(removed, MultiPolygon):
|
|
122
|
+
candidates = list(removed.geoms)
|
|
123
|
+
else:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
for geom in candidates:
|
|
127
|
+
if geom.area < min_area:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if min_aspect_ratio:
|
|
131
|
+
minx, miny, maxx, maxy = geom.bounds
|
|
132
|
+
width = maxx - minx
|
|
133
|
+
height = maxy - miny
|
|
134
|
+
aspect_ratio = max(width, height) / max(min(width, height), 1e-6)
|
|
135
|
+
|
|
136
|
+
if aspect_ratio >= min_aspect_ratio:
|
|
137
|
+
filtered_parts.append(geom)
|
|
138
|
+
else:
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
filtered_parts.append(geom)
|
|
142
|
+
|
|
143
|
+
return unary_union(filtered_parts) if filtered_parts else None
|
|
144
|
+
|
|
145
|
+
def compute_adaptive_buffer_dist(polygon, fraction=0.004):
|
|
146
|
+
"""
|
|
147
|
+
Compute an adaptive buffer distance based on polygon scale.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
- polygon: shapely Polygon or MultiPolygon
|
|
151
|
+
- fraction: proportion of diagonal length (e.g. 0.004 = 0.4%)
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
- buffer distance (float)
|
|
155
|
+
"""
|
|
156
|
+
minx, miny, maxx, maxy = polygon.bounds
|
|
157
|
+
width = maxx - minx
|
|
158
|
+
height = maxy - miny
|
|
159
|
+
diag = (width**2 + height**2)**0.5
|
|
160
|
+
|
|
161
|
+
return fraction * diag
|
|
162
|
+
|
|
163
|
+
def widen_narrow_parts(polygon, fraction=0.0043, min_area_fraction=1e-8, min_aspect_ratio=None):
|
|
164
|
+
"""
|
|
165
|
+
Widen narrow regions removed during negative buffering using adaptive buffer distance.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
- polygon: shapely Polygon or MultiPolygon
|
|
169
|
+
- fraction: % of bbox diagonal to use for buffer distance
|
|
170
|
+
- min_area_fraction: threshold to remove small slivers (as % of polygon area)
|
|
171
|
+
- min_aspect_ratio: minimum elongation to classify as narrow feature. If None, it is ignored. Default is None.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
- widened polygon
|
|
175
|
+
"""
|
|
176
|
+
if polygon.is_empty or polygon.area == 0:
|
|
177
|
+
return polygon
|
|
178
|
+
|
|
179
|
+
buffer_dist = compute_adaptive_buffer_dist(polygon, fraction=fraction)
|
|
180
|
+
|
|
181
|
+
# Step 1: Apply inward buffer
|
|
182
|
+
simplified = polygon.buffer(-buffer_dist)
|
|
183
|
+
|
|
184
|
+
if simplified.is_empty:
|
|
185
|
+
return polygon # Avoid error if all geometry disappears
|
|
186
|
+
|
|
187
|
+
# Step 2: Re-expand it back
|
|
188
|
+
expanded = simplified.buffer(buffer_dist)
|
|
189
|
+
|
|
190
|
+
# Step 3: Find what got lost during shrink-expand (mostly narrow parts)
|
|
191
|
+
removed_narrow_parts = polygon.difference(expanded)
|
|
192
|
+
|
|
193
|
+
if removed_narrow_parts.is_empty:
|
|
194
|
+
return polygon
|
|
195
|
+
|
|
196
|
+
# Step 4: Filter the narrow parts from removed ones and Slightly buffer them wider
|
|
197
|
+
# 🔍 Filter out noisy slivers
|
|
198
|
+
filtered = filter_narrow_removed_parts(removed_narrow_parts, min_area=min_area_fraction * polygon.area, min_aspect_ratio=min_aspect_ratio)
|
|
199
|
+
|
|
200
|
+
if filtered:
|
|
201
|
+
widened = filtered.buffer(buffer_dist)
|
|
202
|
+
widened_polygon = unary_union([polygon, widened])
|
|
203
|
+
else:
|
|
204
|
+
widened_polygon = polygon
|
|
205
|
+
|
|
206
|
+
return widened_polygon
|
|
207
|
+
|
|
208
|
+
def simplify_geometry(polygon, shape_index_threshold=800, max_tolerance_fraction=0.05, narrow_portion='widen'):
|
|
209
|
+
"""
|
|
210
|
+
Simplify polygon while preserving topology and ensuring shape index is under control.
|
|
211
|
+
|
|
212
|
+
Parameters:
|
|
213
|
+
- polygon: shapely.geometry.Polygon or MultiPolygon
|
|
214
|
+
- shape_index_threshold: Target upper bound for shape index (lower = simpler)
|
|
215
|
+
- max_tolerance_fraction: Max allowed simplification (as % of bounding box diagonal)
|
|
216
|
+
- narrow_portion: 'clean' to remove narrow connections before simplification or 'widen' to widen the narrow connections. Default is 'widen'.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
- simplified polygon
|
|
220
|
+
"""
|
|
221
|
+
if polygon.is_empty or polygon.area == 0:
|
|
222
|
+
return polygon
|
|
223
|
+
|
|
224
|
+
if narrow_portion=='clean':
|
|
225
|
+
polygon = clean_narrow_necks(polygon)
|
|
226
|
+
else:
|
|
227
|
+
polygon = widen_narrow_parts(polygon)
|
|
228
|
+
|
|
229
|
+
original_si = shape_index(polygon)
|
|
230
|
+
if original_si <= shape_index_threshold:
|
|
231
|
+
return polygon # Already simple enough
|
|
232
|
+
|
|
233
|
+
# Initial tolerance from geometry scale
|
|
234
|
+
initial_tolerance = compute_initial_tolerance(polygon, fraction=0.005)
|
|
235
|
+
max_tolerance = compute_initial_tolerance(polygon, fraction=max_tolerance_fraction)
|
|
236
|
+
tolerance = initial_tolerance
|
|
237
|
+
|
|
238
|
+
# Iteratively simplify until SI drops or max tolerance is reached
|
|
239
|
+
while tolerance <= max_tolerance:
|
|
240
|
+
simplified = polygon.simplify(tolerance, preserve_topology=True)
|
|
241
|
+
si = shape_index(simplified)
|
|
242
|
+
|
|
243
|
+
if si <= shape_index_threshold:
|
|
244
|
+
return simplified
|
|
245
|
+
|
|
246
|
+
if simplified.equals_exact(polygon, tolerance / 10):
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
tolerance *= 2
|
|
250
|
+
|
|
251
|
+
return polygon
|
|
252
|
+
|
|
253
|
+
def simplify_path(points, geometry):
|
|
254
|
+
if not points:
|
|
255
|
+
return points
|
|
256
|
+
simplified = [points[0]]
|
|
257
|
+
i = 0
|
|
258
|
+
while i < len(points) - 1:
|
|
259
|
+
j = len(points) - 1
|
|
260
|
+
while j > i + 1:
|
|
261
|
+
if geometry.contains(LineString([points[i], points[j]])):
|
|
262
|
+
break
|
|
263
|
+
j -= 1
|
|
264
|
+
simplified.append(points[j])
|
|
265
|
+
i = j
|
|
266
|
+
return simplified
|
|
267
|
+
|
|
268
|
+
def create_continuous_linestring(a, b, geometry, resolution=0.01):
|
|
269
|
+
"""
|
|
270
|
+
Create a continuous LineString between two points that lies entirely within a given geometry.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
a : tuple
|
|
275
|
+
The coordinates of the first point (x, y).
|
|
276
|
+
|
|
277
|
+
b : tuple
|
|
278
|
+
The coordinates of the second point (x, y).
|
|
279
|
+
|
|
280
|
+
geometry : shapely.geometry.Polygon or shapely.geometry.MultiPolygon
|
|
281
|
+
The geometry within which the LineString should be created.
|
|
282
|
+
|
|
283
|
+
resolution : float, optional
|
|
284
|
+
The resolution of the grid used for pathfinding. Default is 0.5.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
shapely.geometry.LineString or None
|
|
289
|
+
A continuous LineString between points `a` and `b` that does not cross the exterior
|
|
290
|
+
boundary of the geometry. If it is not possible to create such a LineString, `None` is returned.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
# Ensure the geometry is oriented counter-clockwise
|
|
294
|
+
geometry = orient(geometry, sign=1.0)
|
|
295
|
+
|
|
296
|
+
# Create a grid over the bounding box of the geometry
|
|
297
|
+
minx, miny, maxx, maxy = geometry.bounds
|
|
298
|
+
|
|
299
|
+
# Get bounding box dimensions
|
|
300
|
+
width = maxx - minx
|
|
301
|
+
height = maxy - miny
|
|
302
|
+
|
|
303
|
+
# Compute aspect ratio
|
|
304
|
+
longer, shorter = max(width, height), min(width, height)
|
|
305
|
+
aspect_ratio = longer / shorter if shorter != 0 else 1
|
|
306
|
+
|
|
307
|
+
# Threshold to trigger anisotropic resolution (you can tune this)
|
|
308
|
+
aspect_ratio_threshold = 2
|
|
309
|
+
|
|
310
|
+
if aspect_ratio > aspect_ratio_threshold:
|
|
311
|
+
# Elongated shape — adjust resolution
|
|
312
|
+
if width > height:
|
|
313
|
+
x_res = resolution * 2 # make x_res coarser
|
|
314
|
+
y_res = resolution
|
|
315
|
+
else:
|
|
316
|
+
y_res = resolution * 2 # make y_res coarser
|
|
317
|
+
x_res = resolution
|
|
318
|
+
else:
|
|
319
|
+
x_res = resolution
|
|
320
|
+
y_res = resolution
|
|
321
|
+
|
|
322
|
+
x_coords = np.arange(minx, maxx + x_res, x_res)
|
|
323
|
+
y_coords = np.arange(miny, maxy + y_res, y_res)
|
|
324
|
+
|
|
325
|
+
# Create points on the grid and filter those within the geometry
|
|
326
|
+
points = []
|
|
327
|
+
for x in x_coords:
|
|
328
|
+
for y in y_coords:
|
|
329
|
+
p = Point(x, y)
|
|
330
|
+
if geometry.contains(p):
|
|
331
|
+
points.append(p)
|
|
332
|
+
|
|
333
|
+
# Debug: #Print number of valid points
|
|
334
|
+
#print(f"Number of valid points within the geometry: {len(points)}")
|
|
335
|
+
|
|
336
|
+
# Check if any valid points exist
|
|
337
|
+
if not points:
|
|
338
|
+
raise ValueError("No valid points within the geometry. Adjust resolution or check the geometry.")
|
|
339
|
+
|
|
340
|
+
# Create a spatial index for the points
|
|
341
|
+
tree = STRtree(points)
|
|
342
|
+
|
|
343
|
+
# Build a dict of coordinate → Point
|
|
344
|
+
point_dict = {(round(p.x, 6), round(p.y, 6)): p for p in points}
|
|
345
|
+
|
|
346
|
+
# Create a graph for pathfinding using Dijkstra's algorithm
|
|
347
|
+
graph = nx.Graph()
|
|
348
|
+
|
|
349
|
+
# Add nodes to the graph
|
|
350
|
+
for point in points:
|
|
351
|
+
graph.add_node((point.x, point.y))
|
|
352
|
+
|
|
353
|
+
# Connect adjacent points in the grid that are within the geometry (without STRtree: Old method; not working for sharp narrow turns in polygons)
|
|
354
|
+
# for point in points:
|
|
355
|
+
# x, y = point.x, point.y
|
|
356
|
+
# # possible_neighbors = [(x + dx, y + dy) for dx in [-resolution, 0, resolution] for dy in [-resolution, 0, resolution] if (dx, dy) != (0, 0)]
|
|
357
|
+
# radius = 5 # or 3, depending on resolution and geometry size
|
|
358
|
+
# possible_neighbors = [
|
|
359
|
+
# (x + dx * x_res, y + dy * y_res)
|
|
360
|
+
# for dx in range(-radius, radius + 1)
|
|
361
|
+
# for dy in range(-radius, radius + 1)
|
|
362
|
+
# if not (dx == 0 and dy == 0)
|
|
363
|
+
# ]
|
|
364
|
+
# for (neighbor_x, neighbor_y) in possible_neighbors:
|
|
365
|
+
# neighbor = Point(neighbor_x, neighbor_y)
|
|
366
|
+
# # Then during neighbor search:
|
|
367
|
+
# coord = (round(neighbor_x, 6), round(neighbor_y, 6))
|
|
368
|
+
# if coord in point_dict:
|
|
369
|
+
# line = LineString([point, neighbor])
|
|
370
|
+
# if geometry.covers(line):
|
|
371
|
+
# distance = point.distance(neighbor)
|
|
372
|
+
# #print(f"Adding edge between {point} and {neighbor} with distance {distance}")
|
|
373
|
+
# graph.add_edge((point.x, point.y), (neighbor.x, neighbor.y), weight=distance)
|
|
374
|
+
|
|
375
|
+
for point in points:
|
|
376
|
+
# Find nearby geometries within a buffer radius
|
|
377
|
+
search_radius = max(x_res, y_res) * 2.5
|
|
378
|
+
nearby = tree.query(point.buffer(search_radius))
|
|
379
|
+
for idx in nearby:
|
|
380
|
+
neighbor = points[idx] # Index into your original list
|
|
381
|
+
if point.equals(neighbor):
|
|
382
|
+
continue
|
|
383
|
+
line = LineString([point, neighbor])
|
|
384
|
+
if geometry.covers(line):
|
|
385
|
+
distance = point.distance(neighbor)
|
|
386
|
+
graph.add_edge((point.x, point.y), (neighbor.x, neighbor.y), weight=distance)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# If the graph is empty, return None
|
|
390
|
+
if graph.number_of_nodes() == 0:
|
|
391
|
+
raise ValueError("Graph is empty. No valid paths could be constructed within the geometry.")
|
|
392
|
+
|
|
393
|
+
# Convert points a and b to nearest grid points
|
|
394
|
+
start = Point(a)
|
|
395
|
+
end = Point(b)
|
|
396
|
+
|
|
397
|
+
# Check if the exact point exists in the graph, if not, find the nearest one
|
|
398
|
+
nearest_start = min(graph.nodes, key=lambda n: start.distance(Point(n)), default=None)
|
|
399
|
+
nearest_end = min(graph.nodes, key=lambda n: end.distance(Point(n)), default=None)
|
|
400
|
+
|
|
401
|
+
if nearest_start is None or nearest_end is None:
|
|
402
|
+
raise ValueError("Could not find nearest grid points within the graph. Adjust resolution or check the geometry.")
|
|
403
|
+
|
|
404
|
+
# Find the shortest path within the graph using Dijkstra's algorithm
|
|
405
|
+
try:
|
|
406
|
+
path_coords = nx.dijkstra_path(graph, nearest_start, nearest_end, weight='weight')
|
|
407
|
+
path = [Point(x, y) for x, y in path_coords]
|
|
408
|
+
path.insert(0, start) # Add the start point
|
|
409
|
+
path.append(end) # Add the end point
|
|
410
|
+
simplified_path = simplify_path(path, geometry)
|
|
411
|
+
return LineString(simplified_path), graph
|
|
412
|
+
# return LineString(path), graph
|
|
413
|
+
except nx.NetworkXNoPath:
|
|
414
|
+
return None, graph
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def find_actual_flow_path(dam_point, reservoir_polygon, inlet_point=None, resolution=None):
|
|
419
|
+
"""
|
|
420
|
+
This function finds the actual flow path between a dam point and the inlet of the reservoir boundary within the polygon.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
dam_point (Point): The point representing the dam location.
|
|
424
|
+
reservoir_polygon (Polygon): The polygon representing the reservoir boundary.
|
|
425
|
+
inlet_point (Point, optional): The point representing the inlet of the reservoir. If not provided, the farthest point on the simplified reservoir boundary is used.
|
|
426
|
+
resolution (float, optional): The resolution to use for simplifying the reservoir boundary. If not provided, the optimal resolution is found.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
simplified_reservoir (Polygon): The simplified reservoir boundary.
|
|
430
|
+
far_end_point (Point): The farthest point on the simplified reservoir boundary.
|
|
431
|
+
path (LineString): The shortest path between the dam point and the far end point.
|
|
432
|
+
graph (Graph): The graph used for pathfinding.
|
|
433
|
+
"""
|
|
434
|
+
# Step 1: Simplify the reservoir boundary
|
|
435
|
+
simplified_reservoir = simplify_geometry(reservoir_polygon)
|
|
436
|
+
|
|
437
|
+
# Step 2: Find optimal resolution for simplified geometry
|
|
438
|
+
if resolution:
|
|
439
|
+
optimal_resolution = resolution
|
|
440
|
+
else:
|
|
441
|
+
optimal_resolution = find_optimal_resolution(simplified_reservoir)
|
|
442
|
+
|
|
443
|
+
# Step 3: Identify the farthest point on the simplified boundary if inlet point not given.
|
|
444
|
+
if inlet_point:
|
|
445
|
+
farthest_point = inlet_point
|
|
446
|
+
far_end_point = Point(farthest_point)
|
|
447
|
+
else:
|
|
448
|
+
main_polygon = get_largest_polygon(simplified_reservoir)
|
|
449
|
+
farthest_point = max(main_polygon.exterior.coords, key=lambda coord: dam_point.distance(Point(coord)))
|
|
450
|
+
far_end_point = Point(farthest_point)
|
|
451
|
+
|
|
452
|
+
# Step4: Find the shortest path within geometry between dam_point and far_end_point
|
|
453
|
+
path, graph = create_continuous_linestring(dam_point,far_end_point, simplified_reservoir, optimal_resolution)
|
|
454
|
+
|
|
455
|
+
return simplified_reservoir,far_end_point, path, graph
|