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