ras-commander 0.48.0__py3-none-any.whl → 0.49.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.
@@ -1,13 +1,29 @@
1
+ """
2
+ Class: HdfFluvialPluvial
3
+
4
+ All of the methods in this class are static and are designed to be used without instantiation.
5
+
6
+ List of Functions in HdfFluvialPluvial:
7
+ - calculate_fluvial_pluvial_boundary()
8
+ - _process_cell_adjacencies()
9
+ - _identify_boundary_edges()
10
+
11
+ """
12
+
1
13
  from typing import Dict, List, Tuple
2
14
  import pandas as pd
3
15
  import geopandas as gpd
4
- import matplotlib.pyplot as plt
5
16
  from collections import defaultdict
6
- from shapely.geometry import LineString, MultiLineString
17
+ from shapely.geometry import LineString
7
18
  from tqdm import tqdm
8
-
9
19
  from .HdfMesh import HdfMesh
10
20
  from .HdfUtils import HdfUtils
21
+ from .Decorators import standardize_input
22
+ from .HdfResultsMesh import HdfResultsMesh
23
+ from .LoggingConfig import get_logger
24
+ from pathlib import Path
25
+
26
+ logger = get_logger(__name__)
11
27
 
12
28
  class HdfFluvialPluvial:
13
29
  """
@@ -15,234 +31,141 @@ class HdfFluvialPluvial:
15
31
 
16
32
  This class provides methods to process and visualize HEC-RAS 2D model outputs,
17
33
  specifically focusing on the delineation of fluvial and pluvial flood areas.
18
- It includes functionality for plotting maximum water surface elevations,
19
- extracting cell polygons, and calculating fluvial-pluvial boundaries based on
34
+ It includes functionality for calculating fluvial-pluvial boundaries based on
20
35
  the timing of maximum water surface elevations.
21
36
 
22
- Key Features:
23
- - Plotting maximum water surface elevations and their timing
24
- - Extracting and visualizing 2D flow area cell polygons
25
- - Calculating and visualizing fluvial-pluvial boundaries
37
+ Key Concepts:
38
+ - Fluvial flooding: Flooding from rivers/streams
39
+ - Pluvial flooding: Flooding from rainfall/surface water
40
+ - Delta_t: Time threshold (in hours) used to distinguish between fluvial and pluvial cells.
41
+ Cells with max WSE time differences greater than delta_t are considered boundaries.
26
42
 
27
43
  Data Requirements:
28
- 1. For plotting maximum water surface:
29
- - Use HdfResultsMesh.mesh_max_ws(hdf_path) to get max_ws_df
30
- - Use HdfResultsMesh.mesh_timeseries_output(hdf_path, mesh_name, 'water_surface')
31
- to get time series data
32
-
33
- 2. For extracting cell polygons:
34
- - Use HdfMesh.mesh_cell_polygons(geom_hdf_path) to get cell_polygons_df
35
- - Use HdfUtils.projection(hdf_path) to get the projection
36
-
37
- 3. For calculating fluvial-pluvial boundary:
38
- - Requires cell_polygons_gdf (from step 2)
39
- - Requires max_ws_df with 'cell_id' and 'max_wsel_time' columns
40
- (can be derived from HdfResultsMesh.mesh_max_ws(hdf_path))
41
-
42
- Usage:
43
- To use this class effectively, first initialize a RasPrj object and load the
44
- necessary HDF files. Then, use the methods provided to analyze and visualize
45
- the fluvial-pluvial characteristics of your 2D model results.
46
-
47
- Example:
48
- ras = RasPrj()
49
- ras = init_ras_project(project_path, ras_version)
50
- hdf_path = ras.get_plan_value(plan_number, 'Results_Output')
51
-
52
- # Get maximum water surface data
53
- max_ws_df = HdfResultsMesh.mesh_max_ws(hdf_path)
54
-
55
- # Plot maximum water surface
56
- HdfFluvialPluvial.plot_max_water_surface(max_ws_df)
57
-
58
- # Extract cell polygons
59
- cell_polygons_df = HdfMesh.mesh_cell_polygons(hdf_path)
60
- projection = HdfUtils.projection(hdf_path)
61
- cell_polygons_gdf = HdfFluvialPluvial.plot_cell_polygons(cell_polygons_df, projection)
62
-
63
- # Calculate fluvial-pluvial boundary
64
- boundary_gdf = HdfFluvialPluvial.calculate_fluvial_pluvial_boundary(cell_polygons_gdf, max_ws_df)
65
-
66
- Note: Ensure that you have the necessary permissions and have initialized
67
- the RAS project correctly before attempting to access HDF files.
44
+ - HEC-RAS plan HDF file containing:
45
+ - 2D mesh cell geometry (accessed via HdfMesh)
46
+ - Maximum water surface elevation times (accessed via HdfResultsMesh)
47
+
48
+ Usage Example:
49
+ >>> ras = init_ras_project(project_path, ras_version)
50
+ >>> hdf_path = Path("path/to/plan.hdf")
51
+ >>> boundary_gdf = HdfFluvialPluvial.calculate_fluvial_pluvial_boundary(
52
+ ... hdf_path,
53
+ ... delta_t=12
54
+ ... )
68
55
  """
69
56
 
70
57
  @staticmethod
71
- def plot_max_water_surface(max_ws_df):
72
- """
73
- Plots the maximum water surface elevation per cell.
74
-
75
- Parameters:
76
- - max_ws_df: DataFrame containing merged data with coordinates and max water surface.
77
-
78
- Returns:
79
- - None
80
- """
81
- # Extract x and y coordinates from the geometry column
82
- max_ws_df['x'] = max_ws_df['geometry'].apply(lambda geom: geom.x if geom is not None else None)
83
- max_ws_df['y'] = max_ws_df['geometry'].apply(lambda geom: geom.y if geom is not None else None)
84
-
85
- # Check if 'x' and 'y' columns exist in max_ws_df
86
- if 'x' not in max_ws_df.columns or 'y' not in max_ws_df.columns:
87
- print("Error: 'x' or 'y' columns not found in the merged dataframe.")
88
- print("Available columns:", max_ws_df.columns.tolist())
89
- return
90
-
91
- # Create the plot
92
- fig, ax = plt.subplots(figsize=(12, 8))
93
- scatter = ax.scatter(max_ws_df['x'], max_ws_df['y'], c=max_ws_df['maximum_water_surface'], cmap='viridis', s=10)
94
-
95
- # Customize the plot
96
- ax.set_title('Max Water Surface per Cell')
97
- ax.set_xlabel('X Coordinate')
98
- ax.set_ylabel('Y Coordinate')
99
- plt.colorbar(scatter, label='Max Water Surface (ft)')
100
-
101
- # Add grid lines
102
- ax.grid(True, linestyle='--', alpha=0.7)
103
-
104
- # Increase font size for better readability
105
- plt.rcParams.update({'font.size': 12})
106
-
107
- # Adjust layout to prevent cutting off labels
108
- plt.tight_layout()
109
-
110
- # Show the plot
111
- plt.show()
112
-
113
-
114
-
115
-
116
- @staticmethod
117
- def plot_max_wsel_time(max_ws_df: pd.DataFrame) -> None:
118
- """
119
- Plots the time of the maximum water surface elevation (WSEL) per cell.
120
-
121
- Parameters:
122
- - max_ws_df: DataFrame containing merged data with coordinates and max water surface.
123
-
124
- Returns:
125
- - None
126
- """
127
- max_ws_df['max_wsel_time'] = pd.to_datetime(max_ws_df['maximum_water_surface_time'])
128
- HdfFluvialPluvial._extract_coordinates(max_ws_df)
129
-
130
- if 'x' not in max_ws_df.columns or 'y' not in max_ws_df.columns:
131
- raise ValueError("x and y coordinates are missing from the DataFrame. Make sure the 'face_point' column exists and contains valid coordinate data.")
132
-
133
- fig, ax = plt.subplots(figsize=(12, 8))
134
-
135
- min_time = max_ws_df['max_wsel_time'].min()
136
- color_values = (max_ws_df['max_wsel_time'] - min_time).dt.total_seconds() / 3600
137
-
138
- scatter = ax.scatter(max_ws_df['x'], max_ws_df['y'], c=color_values, cmap='viridis', s=10)
139
-
140
- ax.set_title('Time of Maximum Water Surface Elevation per Cell')
141
- ax.set_xlabel('X Coordinate')
142
- ax.set_ylabel('Y Coordinate')
143
-
144
- cbar = plt.colorbar(scatter)
145
- cbar.set_label('Hours since simulation start')
146
- cbar.set_ticks(range(0, int(color_values.max()) + 1, 6))
147
- cbar.set_ticklabels([f'{h}h' for h in range(0, int(color_values.max()) + 1, 6)])
148
-
149
- ax.grid(True, linestyle='--', alpha=0.7)
150
- plt.rcParams.update({'font.size': 12})
151
- plt.tight_layout()
152
- plt.show()
153
-
154
- HdfFluvialPluvial._print_max_wsel_info(max_ws_df, min_time)
155
-
156
- @staticmethod
157
- def plot_cell_polygons(cell_polygons_df: pd.DataFrame, projection: str) -> gpd.GeoDataFrame:
58
+ @standardize_input(file_type='plan_hdf')
59
+ def calculate_fluvial_pluvial_boundary(hdf_path: Path, delta_t: float = 12) -> gpd.GeoDataFrame:
158
60
  """
159
- Plots the cell polygons from the provided DataFrame and returns the GeoDataFrame.
61
+ Calculate the fluvial-pluvial boundary based on cell polygons and maximum water surface elevation times.
160
62
 
161
63
  Args:
162
- cell_polygons_df (pd.DataFrame): DataFrame containing cell polygons.
163
- projection (str): The coordinate reference system to assign to the GeoDataFrame.
64
+ hdf_path (Path): Path to the HEC-RAS plan HDF file
65
+ delta_t (float): Threshold time difference in hours. Cells with time differences
66
+ greater than this value are considered boundaries. Default is 12 hours.
164
67
 
165
68
  Returns:
166
- gpd.GeoDataFrame: GeoDataFrame containing the cell polygons.
69
+ gpd.GeoDataFrame: GeoDataFrame containing the fluvial-pluvial boundaries with:
70
+ - geometry: LineString features representing boundaries
71
+ - CRS: Coordinate reference system matching the input HDF file
72
+
73
+ Raises:
74
+ ValueError: If no cell polygons or maximum water surface data found in HDF file
75
+ Exception: If there are errors during boundary calculation
76
+
77
+ Note:
78
+ The returned boundaries represent locations where the timing of maximum water surface
79
+ elevation changes significantly (> delta_t), indicating potential transitions between
80
+ fluvial and pluvial flooding mechanisms.
167
81
  """
168
- if cell_polygons_df.empty:
169
- print("No Cell Polygons found.")
170
- return None
82
+ try:
83
+ # Get cell polygons from HdfMesh
84
+ logger.info("Getting cell polygons from HDF file...")
85
+ cell_polygons_gdf = HdfMesh.get_mesh_cell_polygons(hdf_path)
86
+ if cell_polygons_gdf.empty:
87
+ raise ValueError("No cell polygons found in HDF file")
88
+
89
+ # Get max water surface data from HdfResultsMesh
90
+ logger.info("Getting maximum water surface data from HDF file...")
91
+ max_ws_df = HdfResultsMesh.get_mesh_max_ws(hdf_path)
92
+ if max_ws_df.empty:
93
+ raise ValueError("No maximum water surface data found in HDF file")
94
+
95
+ # Convert timestamps using the renamed utility function
96
+ if 'maximum_water_surface_time' in max_ws_df.columns:
97
+ max_ws_df['maximum_water_surface_time'] = max_ws_df['maximum_water_surface_time'].apply(
98
+ lambda x: HdfUtils.parse_ras_datetime(x) if isinstance(x, str) else x
99
+ )
100
+
101
+ # Process cell adjacencies
102
+ cell_adjacency, common_edges = HdfFluvialPluvial._process_cell_adjacencies(cell_polygons_gdf)
103
+
104
+ # Get cell times from max_ws_df
105
+ cell_times = max_ws_df.set_index('cell_id')['maximum_water_surface_time'].to_dict()
106
+
107
+ # Identify boundary edges
108
+ boundary_edges = HdfFluvialPluvial._identify_boundary_edges(
109
+ cell_adjacency, common_edges, cell_times, delta_t
110
+ )
111
+
112
+ # Join adjacent LineStrings into simple LineStrings
113
+ joined_lines = []
114
+ current_line = []
115
+
116
+ for edge in boundary_edges:
117
+ if not current_line:
118
+ current_line.append(edge)
119
+ else:
120
+ if current_line[-1].coords[-1] == edge.coords[0]:
121
+ current_line.append(edge)
122
+ else:
123
+ joined_lines.append(LineString([point for line in current_line for point in line.coords]))
124
+ current_line = [edge]
125
+
126
+ if current_line:
127
+ joined_lines.append(LineString([point for line in current_line for point in line.coords]))
171
128
 
172
- cell_polygons_gdf = HdfFluvialPluvial._convert_to_geodataframe(cell_polygons_df, projection)
129
+ # Create final GeoDataFrame with CRS from cell_polygons_gdf
130
+ boundary_gdf = gpd.GeoDataFrame(
131
+ geometry=joined_lines,
132
+ crs=cell_polygons_gdf.crs
133
+ )
173
134
 
174
- print("Cell Polygons CRS:", cell_polygons_gdf.crs)
175
- display(cell_polygons_gdf.head())
135
+ # Clean up intermediate dataframes
136
+ del cell_polygons_gdf
137
+ del max_ws_df
176
138
 
177
- fig, ax = plt.subplots(figsize=(12, 8))
178
- cell_polygons_gdf.plot(ax=ax, edgecolor='blue', facecolor='none')
179
- ax.set_xlabel('X Coordinate')
180
- ax.set_ylabel('Y Coordinate')
181
- ax.set_title('2D Flow Area Cell Polygons')
182
- ax.grid(True)
183
- plt.tight_layout()
184
- plt.show()
139
+ return boundary_gdf
185
140
 
186
- return cell_polygons_gdf
141
+ except Exception as e:
142
+ logger.error(f"Error calculating fluvial-pluvial boundary: {str(e)}")
143
+ raise
187
144
 
188
145
  @staticmethod
189
- def calculate_fluvial_pluvial_boundary(cell_polygons_gdf: gpd.GeoDataFrame, max_ws_df: pd.DataFrame, delta_t: float = 12) -> gpd.GeoDataFrame:
146
+ def _process_cell_adjacencies(cell_polygons_gdf: gpd.GeoDataFrame) -> Tuple[Dict[int, List[int]], Dict[int, Dict[int, LineString]]]:
190
147
  """
191
- Calculate the fluvial-pluvial boundary based on cell polygons and maximum water surface elevation times.
148
+ Process cell adjacencies and common edges using R-tree spatial indexing for efficiency.
192
149
 
193
150
  Args:
194
- cell_polygons_gdf (gpd.GeoDataFrame): GeoDataFrame containing cell polygons with 'cell_id' and 'geometry' columns.
195
- max_ws_df (pd.DataFrame): DataFrame containing 'cell_id' and 'max_wsel_time' columns.
196
- delta_t (float): Threshold time difference in hours. Default is 12 hours.
151
+ cell_polygons_gdf (gpd.GeoDataFrame): GeoDataFrame containing 2D mesh cell polygons
152
+ with 'cell_id' and 'geometry' columns
197
153
 
198
154
  Returns:
199
- gpd.GeoDataFrame: GeoDataFrame containing the fluvial-pluvial boundary as simple LineStrings.
155
+ Tuple containing:
156
+ - Dict[int, List[int]]: Dictionary mapping cell IDs to lists of adjacent cell IDs
157
+ - Dict[int, Dict[int, LineString]]: Nested dictionary storing common edges between cells,
158
+ where common_edges[cell1][cell2] gives the shared boundary
159
+
160
+ Note:
161
+ Uses R-tree spatial indexing to efficiently identify potential neighboring cells
162
+ before performing more detailed geometric operations.
200
163
  """
201
- cell_adjacency, common_edges = HdfFluvialPluvial._process_cell_adjacencies(cell_polygons_gdf)
202
- cell_times = max_ws_df.set_index('cell_id')['max_wsel_time'].to_dict()
203
- boundary_edges = HdfFluvialPluvial._identify_boundary_edges(cell_adjacency, common_edges, cell_times, delta_t)
204
-
205
- # Join adjacent LineStrings into simple LineStrings
206
- joined_lines = []
207
- current_line = []
208
-
209
- for edge in boundary_edges:
210
- if not current_line:
211
- current_line.append(edge)
212
- else:
213
- if current_line[-1].coords[-1] == edge.coords[0]: # Check if the last point of the current line matches the first point of the new edge
214
- current_line.append(edge)
215
- else:
216
- # Create a simple LineString from the current line and reset
217
- joined_lines.append(LineString([point for line in current_line for point in line.coords]))
218
- current_line = [edge]
219
-
220
- # Add the last collected line if exists
221
- if current_line:
222
- joined_lines.append(LineString([point for line in current_line for point in line.coords]))
223
-
224
- boundary_gdf = gpd.GeoDataFrame(geometry=joined_lines, crs=cell_polygons_gdf.crs)
225
- return boundary_gdf
226
-
227
- @staticmethod
228
- def _print_max_wsel_info(max_ws_df: pd.DataFrame, min_time: pd.Timestamp) -> None:
229
- max_wsel_row = max_ws_df.loc[max_ws_df['maximum_water_surface'].idxmax()]
230
- hours_since_start = (max_wsel_row['max_wsel_time'] - min_time).total_seconds() / 3600
231
- print(f"\nOverall Maximum WSEL: {max_wsel_row['maximum_water_surface']:.2f} ft")
232
- print(f"Time of Overall Maximum WSEL: {max_wsel_row['max_wsel_time']}")
233
- print(f"Hours since simulation start: {hours_since_start:.2f} hours")
234
- print(f"Location of Overall Maximum WSEL: X={max_wsel_row['x']}, Y={max_wsel_row['y']}")
235
-
236
- @staticmethod
237
- def _process_cell_adjacencies(cell_polygons_gdf: gpd.GeoDataFrame) -> Tuple[Dict[int, List[int]], Dict[int, Dict[int, LineString]]]:
238
- """
239
- Process cell adjacencies and common edges using R-tree indexing.
240
- """
241
- # Install rtree using pip install rtree if not already installed
242
164
  from rtree import index
243
165
  cell_adjacency = defaultdict(list)
244
166
  common_edges = defaultdict(dict)
245
167
  idx = index.Index()
168
+
246
169
  for i, geom in enumerate(cell_polygons_gdf.geometry):
247
170
  idx.insert(i, geom.bounds)
248
171
 
@@ -251,15 +174,15 @@ class HdfFluvialPluvial:
251
174
  cell_id1 = row1['cell_id']
252
175
  poly1 = row1['geometry']
253
176
  potential_neighbors = list(idx.intersection(poly1.bounds))
254
-
177
+
255
178
  for idx2 in potential_neighbors:
256
179
  if idx1 >= idx2:
257
180
  continue
258
-
181
+
259
182
  row2 = cell_polygons_gdf.iloc[idx2]
260
183
  cell_id2 = row2['cell_id']
261
184
  poly2 = row2['geometry']
262
-
185
+
263
186
  if poly1.touches(poly2):
264
187
  intersection = poly1.intersection(poly2)
265
188
  if isinstance(intersection, LineString):
@@ -267,60 +190,45 @@ class HdfFluvialPluvial:
267
190
  cell_adjacency[cell_id2].append(cell_id1)
268
191
  common_edges[cell_id1][cell_id2] = intersection
269
192
  common_edges[cell_id2][cell_id1] = intersection
270
-
193
+
271
194
  pbar.update(1)
272
-
195
+
273
196
  return cell_adjacency, common_edges
274
197
 
275
198
  @staticmethod
276
- def _identify_boundary_edges(cell_adjacency: Dict[int, List[int]], common_edges: Dict[int, Dict[int, LineString]], cell_times: Dict[int, pd.Timestamp], delta_t: float) -> List[LineString]:
199
+ def _identify_boundary_edges(cell_adjacency: Dict[int, List[int]],
200
+ common_edges: Dict[int, Dict[int, LineString]],
201
+ cell_times: Dict[int, pd.Timestamp],
202
+ delta_t: float) -> List[LineString]:
203
+ """
204
+ Identify boundary edges between cells with significant time differences.
205
+
206
+ Args:
207
+ cell_adjacency (Dict[int, List[int]]): Dictionary of cell adjacencies
208
+ common_edges (Dict[int, Dict[int, LineString]]): Dictionary of shared edges between cells
209
+ cell_times (Dict[int, pd.Timestamp]): Dictionary mapping cell IDs to their max WSE times
210
+ delta_t (float): Time threshold in hours
211
+
212
+ Returns:
213
+ List[LineString]: List of LineString geometries representing boundaries where
214
+ adjacent cells have time differences greater than delta_t
215
+
216
+ Note:
217
+ Boundaries are identified where the absolute time difference between adjacent
218
+ cells exceeds the specified delta_t threshold.
219
+ """
277
220
  boundary_edges = []
278
221
  with tqdm(total=len(cell_adjacency), desc="Processing cell adjacencies") as pbar:
279
222
  for cell_id, neighbors in cell_adjacency.items():
280
223
  cell_time = cell_times[cell_id]
281
-
224
+
282
225
  for neighbor_id in neighbors:
283
226
  neighbor_time = cell_times[neighbor_id]
284
227
  time_diff = abs((cell_time - neighbor_time).total_seconds() / 3600)
285
-
228
+
286
229
  if time_diff >= delta_t:
287
230
  boundary_edges.append(common_edges[cell_id][neighbor_id])
288
-
289
- pbar.update(1)
290
- return boundary_edges
291
-
292
- @staticmethod
293
- def _extract_coordinates(df: pd.DataFrame) -> None:
294
- """
295
- Extract x and y coordinates from the 'face_point' column.
296
-
297
- Parameters:
298
- - df: DataFrame containing the 'face_point' column.
299
-
300
- Returns:
301
- - None (modifies the DataFrame in-place)
302
- """
303
- if 'face_point' in df.columns:
304
- df[['x', 'y']] = df['face_point'].str.strip('()').str.split(',', expand=True).astype(float)
305
- else:
306
- print("Warning: 'face_point' column not found in the DataFrame.")
307
-
308
- @staticmethod
309
- def _convert_to_geodataframe(df: pd.DataFrame, projection: str) -> gpd.GeoDataFrame:
310
- """
311
- Convert a DataFrame to a GeoDataFrame.
312
-
313
- Parameters:
314
- - df: DataFrame containing 'geometry' column.
315
- - projection: The coordinate reference system to assign to the GeoDataFrame.
316
-
317
- Returns:
318
- - GeoDataFrame with the specified projection.
319
- """
320
- gdf = gpd.GeoDataFrame(df, geometry='geometry', crs=projection)
321
-
322
- return gdf
323
-
324
-
325
231
 
232
+ pbar.update(1)
326
233
 
234
+ return boundary_edges