ras-commander 0.45.0__tar.gz → 0.46.0__tar.gz

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.
Files changed (40) hide show
  1. {ras_commander-0.45.0/ras_commander.egg-info → ras_commander-0.46.0}/PKG-INFO +1 -1
  2. ras_commander-0.46.0/ras_commander/HdfFluvialPluvial.py +317 -0
  3. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfMesh.py +62 -15
  4. ras_commander-0.46.0/ras_commander/HdfPipe.py +771 -0
  5. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfPlan.py +5 -0
  6. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfPump.py +25 -11
  7. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfResultsMesh.py +135 -62
  8. ras_commander-0.46.0/ras_commander/HdfResultsXsec.py +272 -0
  9. ras_commander-0.46.0/ras_commander/HdfStruc.py +245 -0
  10. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfUtils.py +51 -0
  11. ras_commander-0.46.0/ras_commander/HdfXsec.py +613 -0
  12. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasPlan.py +298 -45
  13. ras_commander-0.46.0/ras_commander/RasToGo.py +21 -0
  14. ras_commander-0.46.0/ras_commander/RasUnsteady.py +710 -0
  15. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/__init__.py +3 -1
  16. {ras_commander-0.45.0 → ras_commander-0.46.0/ras_commander.egg-info}/PKG-INFO +1 -1
  17. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander.egg-info/SOURCES.txt +2 -0
  18. {ras_commander-0.45.0 → ras_commander-0.46.0}/setup.py +1 -1
  19. ras_commander-0.45.0/ras_commander/HdfPipe.py +0 -262
  20. ras_commander-0.45.0/ras_commander/HdfResultsXsec.py +0 -443
  21. ras_commander-0.45.0/ras_commander/HdfStruc.py +0 -147
  22. ras_commander-0.45.0/ras_commander/HdfXsec.py +0 -282
  23. ras_commander-0.45.0/ras_commander/RasUnsteady.py +0 -109
  24. {ras_commander-0.45.0 → ras_commander-0.46.0}/LICENSE +0 -0
  25. {ras_commander-0.45.0 → ras_commander-0.46.0}/README.md +0 -0
  26. {ras_commander-0.45.0 → ras_commander-0.46.0}/pyproject.toml +0 -0
  27. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/Decorators.py +0 -0
  28. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfBase.py +0 -0
  29. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfBndry.py +0 -0
  30. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/HdfResultsPlan.py +0 -0
  31. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/LoggingConfig.py +0 -0
  32. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasCmdr.py +0 -0
  33. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasExamples.py +0 -0
  34. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasGeo.py +0 -0
  35. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasGpt.py +0 -0
  36. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasPrj.py +0 -0
  37. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander/RasUtils.py +0 -0
  38. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander.egg-info/dependency_links.txt +0 -0
  39. {ras_commander-0.45.0 → ras_commander-0.46.0}/ras_commander.egg-info/top_level.txt +0 -0
  40. {ras_commander-0.45.0 → ras_commander-0.46.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ras-commander
3
- Version: 0.45.0
3
+ Version: 0.46.0
4
4
  Summary: A Python library for automating HEC-RAS operations
5
5
  Home-page: https://github.com/billk-FM/ras-commander
6
6
  Author: William M. Katzenmeyer
@@ -0,0 +1,317 @@
1
+ from typing import Dict, List, Tuple
2
+ import pandas as pd
3
+ import geopandas as gpd
4
+ import matplotlib.pyplot as plt
5
+ from collections import defaultdict
6
+ from rtree import index
7
+ from shapely.geometry import LineString, MultiLineString
8
+ from tqdm import tqdm
9
+
10
+ from .HdfMesh import HdfMesh
11
+ from .HdfUtils import HdfUtils
12
+
13
+ class HdfFluvialPluvial:
14
+ """
15
+ A class for analyzing and visualizing fluvial-pluvial boundaries in HEC-RAS 2D model results.
16
+
17
+ This class provides methods to process and visualize HEC-RAS 2D model outputs,
18
+ 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
21
+ the timing of maximum water surface elevations.
22
+
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
27
+
28
+ 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.
69
+ """
70
+
71
+ @staticmethod
72
+ def plot_max_water_surface(max_ws_df):
73
+ """
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.
161
+
162
+ Args:
163
+ cell_polygons_df (pd.DataFrame): DataFrame containing cell polygons.
164
+ projection (str): The coordinate reference system to assign to the GeoDataFrame.
165
+
166
+ Returns:
167
+ gpd.GeoDataFrame: GeoDataFrame containing the cell polygons.
168
+ """
169
+ if cell_polygons_df.empty:
170
+ print("No Cell Polygons found.")
171
+ return None
172
+
173
+ cell_polygons_gdf = HdfFluvialPluvial._convert_to_geodataframe(cell_polygons_df, projection)
174
+
175
+ print("Cell Polygons CRS:", cell_polygons_gdf.crs)
176
+ display(cell_polygons_gdf.head())
177
+
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()
186
+
187
+ return cell_polygons_gdf
188
+
189
+ @staticmethod
190
+ def calculate_fluvial_pluvial_boundary(cell_polygons_gdf: gpd.GeoDataFrame, max_ws_df: pd.DataFrame, delta_t: float = 12) -> gpd.GeoDataFrame:
191
+ """
192
+ Calculate the fluvial-pluvial boundary based on cell polygons and maximum water surface elevation times.
193
+
194
+ 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.
198
+
199
+ Returns:
200
+ gpd.GeoDataFrame: GeoDataFrame containing the fluvial-pluvial boundary as simple LineStrings.
201
+ """
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]]]:
239
+ cell_adjacency = defaultdict(list)
240
+ common_edges = defaultdict(dict)
241
+ idx = index.Index()
242
+ for i, geom in enumerate(cell_polygons_gdf.geometry):
243
+ idx.insert(i, geom.bounds)
244
+
245
+ with tqdm(total=len(cell_polygons_gdf), desc="Processing cell adjacencies") as pbar:
246
+ for idx1, row1 in cell_polygons_gdf.iterrows():
247
+ cell_id1 = row1['cell_id']
248
+ poly1 = row1['geometry']
249
+ potential_neighbors = list(idx.intersection(poly1.bounds))
250
+
251
+ for idx2 in potential_neighbors:
252
+ if idx1 >= idx2:
253
+ continue
254
+
255
+ row2 = cell_polygons_gdf.iloc[idx2]
256
+ cell_id2 = row2['cell_id']
257
+ poly2 = row2['geometry']
258
+
259
+ if poly1.touches(poly2):
260
+ intersection = poly1.intersection(poly2)
261
+ if isinstance(intersection, LineString):
262
+ cell_adjacency[cell_id1].append(cell_id2)
263
+ cell_adjacency[cell_id2].append(cell_id1)
264
+ common_edges[cell_id1][cell_id2] = intersection
265
+ common_edges[cell_id2][cell_id1] = intersection
266
+
267
+ pbar.update(1)
268
+
269
+ return cell_adjacency, common_edges
270
+
271
+ @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]:
273
+ boundary_edges = []
274
+ with tqdm(total=len(cell_adjacency), desc="Processing cell adjacencies") as pbar:
275
+ for cell_id, neighbors in cell_adjacency.items():
276
+ cell_time = cell_times[cell_id]
277
+
278
+ for neighbor_id in neighbors:
279
+ neighbor_time = cell_times[neighbor_id]
280
+ time_diff = abs((cell_time - neighbor_time).total_seconds() / 3600)
281
+
282
+ if time_diff >= delta_t:
283
+ 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
+
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.
312
+
313
+ Returns:
314
+ - GeoDataFrame with the specified projection.
315
+ """
316
+ gdf = gpd.GeoDataFrame(df, geometry='geometry', crs=projection)
317
+ return gdf
@@ -39,8 +39,6 @@ class HdfMesh:
39
39
  Note: This class relies on HdfBase and HdfUtils for some underlying operations.
40
40
  """
41
41
 
42
- FLOW_AREA_2D_PATH = "Geometry/2D Flow Areas"
43
-
44
42
  def __init__(self):
45
43
  self.logger = logging.getLogger(__name__)
46
44
 
@@ -62,12 +60,12 @@ class HdfMesh:
62
60
  """
63
61
  try:
64
62
  with h5py.File(hdf_path, 'r') as hdf_file:
65
- if HdfMesh.FLOW_AREA_2D_PATH not in hdf_file:
63
+ if "Geometry/2D Flow Areas" not in hdf_file:
66
64
  return list()
67
65
  return list(
68
66
  [
69
67
  HdfUtils.convert_ras_hdf_string(n)
70
- for n in hdf_file[f"{HdfMesh.FLOW_AREA_2D_PATH}/Attributes"][()]["Name"]
68
+ for n in hdf_file["Geometry/2D Flow Areas/Attributes"][()]["Name"]
71
69
  ]
72
70
  )
73
71
  except Exception as e:
@@ -96,7 +94,7 @@ class HdfMesh:
96
94
  if not mesh_area_names:
97
95
  return GeoDataFrame()
98
96
  mesh_area_polygons = [
99
- Polygon(hdf_file[f"{HdfMesh.FLOW_AREA_2D_PATH}/{n}/Perimeter"][()])
97
+ Polygon(hdf_file["Geometry/2D Flow Areas/{}/Perimeter".format(n)][()])
100
98
  for n in mesh_area_names
101
99
  ]
102
100
  return GeoDataFrame(
@@ -134,13 +132,13 @@ class HdfMesh:
134
132
 
135
133
  cell_dict = {"mesh_name": [], "cell_id": [], "geometry": []}
136
134
  for i, mesh_name in enumerate(mesh_area_names):
137
- cell_cnt = hdf_file[f"{HdfMesh.FLOW_AREA_2D_PATH}/Cell Info"][()][i][1]
135
+ cell_cnt = hdf_file["Geometry/2D Flow Areas/Cell Info"][()][i][1]
138
136
  cell_ids = list(range(cell_cnt))
139
137
  cell_face_info = hdf_file[
140
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/Cells Face and Orientation Info"
138
+ "Geometry/2D Flow Areas/{}/Cells Face and Orientation Info".format(mesh_name)
141
139
  ][()]
142
140
  cell_face_values = hdf_file[
143
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/Cells Face and Orientation Values"
141
+ "Geometry/2D Flow Areas/{}/Cells Face and Orientation Values".format(mesh_name)
144
142
  ][()][:, 0]
145
143
  face_id_lists = list(
146
144
  np.vectorize(
@@ -200,8 +198,8 @@ class HdfMesh:
200
198
  return GeoDataFrame()
201
199
  pnt_dict = {"mesh_name": [], "cell_id": [], "geometry": []}
202
200
  for i, mesh_name in enumerate(mesh_area_names):
203
- starting_row, count = hdf_file[f"{HdfMesh.FLOW_AREA_2D_PATH}/Cell Info"][()][i]
204
- cell_pnt_coords = hdf_file[f"{HdfMesh.FLOW_AREA_2D_PATH}/Cell Points"][()][
201
+ starting_row, count = hdf_file["Geometry/2D Flow Areas/Cell Info"][()][i]
202
+ cell_pnt_coords = hdf_file["Geometry/2D Flow Areas/Cell Points"][()][
205
203
  starting_row : starting_row + count
206
204
  ]
207
205
  pnt_dict["mesh_name"] += [mesh_name] * cell_pnt_coords.shape[0]
@@ -240,16 +238,16 @@ class HdfMesh:
240
238
  face_dict = {"mesh_name": [], "face_id": [], "geometry": []}
241
239
  for mesh_name in mesh_area_names:
242
240
  facepoints_index = hdf_file[
243
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/Faces FacePoint Indexes"
241
+ "Geometry/2D Flow Areas/{}/Faces FacePoint Indexes".format(mesh_name)
244
242
  ][()]
245
243
  facepoints_coordinates = hdf_file[
246
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/FacePoints Coordinate"
244
+ "Geometry/2D Flow Areas/{}/FacePoints Coordinate".format(mesh_name)
247
245
  ][()]
248
246
  faces_perimeter_info = hdf_file[
249
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/Faces Perimeter Info"
247
+ "Geometry/2D Flow Areas/{}/Faces Perimeter Info".format(mesh_name)
250
248
  ][()]
251
249
  faces_perimeter_values = hdf_file[
252
- f"{HdfMesh.FLOW_AREA_2D_PATH}/{mesh_name}/Faces Perimeter Values"
250
+ "Geometry/2D Flow Areas/{}/Faces Perimeter Values".format(mesh_name)
253
251
  ][()]
254
252
  face_id = -1
255
253
  for pnt_a_index, pnt_b_index in facepoints_index:
@@ -288,7 +286,7 @@ class HdfMesh:
288
286
  """
289
287
  try:
290
288
  with h5py.File(hdf_path, 'r') as hdf_file:
291
- d2_flow_area = hdf_file.get(f"{HdfMesh.FLOW_AREA_2D_PATH}/Attributes")
289
+ d2_flow_area = hdf_file.get("Geometry/2D Flow Areas/Attributes")
292
290
  if d2_flow_area is not None and isinstance(d2_flow_area, h5py.Dataset):
293
291
  result = {}
294
292
  for name in d2_flow_area.dtype.names:
@@ -306,3 +304,52 @@ class HdfMesh:
306
304
  except Exception as e:
307
305
  logger.error(f"Error reading 2D flow area attributes from {hdf_path}: {str(e)}")
308
306
  return {}
307
+
308
+
309
+ @staticmethod
310
+ @standardize_input(file_type='geom_hdf')
311
+ def get_face_property_tables(hdf_path: Path) -> Dict[str, pd.DataFrame]:
312
+ """
313
+ Extract Face Property Tables for each Face in all 2D Flow Areas.
314
+
315
+ Parameters
316
+ ----------
317
+ hdf_path : Path
318
+ Path to the HEC-RAS geometry HDF file.
319
+
320
+ Returns
321
+ -------
322
+ Dict[str, pd.DataFrame]
323
+ A dictionary where keys are mesh names and values are DataFrames
324
+ containing the Face Property Tables for all faces in that mesh.
325
+ """
326
+ try:
327
+ with h5py.File(hdf_path, 'r') as hdf_file:
328
+ mesh_area_names = HdfMesh.mesh_area_names(hdf_path)
329
+ if not mesh_area_names:
330
+ return {}
331
+
332
+ result = {}
333
+ for mesh_name in mesh_area_names:
334
+ area_elevation_info = hdf_file[f"Geometry/2D Flow Areas/{mesh_name}/Faces Area Elevation Info"][()]
335
+ area_elevation_values = hdf_file[f"Geometry/2D Flow Areas/{mesh_name}/Faces Area Elevation Values"][()]
336
+
337
+ face_data = []
338
+ for face_id, (start_index, count) in enumerate(area_elevation_info):
339
+ face_values = area_elevation_values[start_index:start_index+count]
340
+ for z, area, wetted_perimeter, mannings_n in face_values:
341
+ face_data.append({
342
+ 'Face ID': face_id,
343
+ 'Z': z,
344
+ 'Area': area,
345
+ 'Wetted Perimeter': wetted_perimeter,
346
+ "Manning's n": mannings_n
347
+ })
348
+
349
+ result[mesh_name] = pd.DataFrame(face_data)
350
+
351
+ return result
352
+
353
+ except Exception as e:
354
+ logger.error(f"Error extracting face property tables from {hdf_path}: {str(e)}")
355
+ return {}