ras-commander 0.33.0__py3-none-any.whl → 0.35.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.
- ras_commander/RasCmdr.py +171 -138
- ras_commander/RasExamples.py +334 -120
- ras_commander/RasGeo.py +27 -6
- ras_commander/RasHdf.py +1702 -0
- ras_commander/RasPlan.py +398 -437
- ras_commander/RasPrj.py +403 -65
- ras_commander/RasUnsteady.py +24 -4
- ras_commander/RasUtils.py +352 -51
- ras_commander/__init__.py +4 -1
- ras_commander-0.35.0.dist-info/METADATA +319 -0
- ras_commander-0.35.0.dist-info/RECORD +15 -0
- ras_commander-0.33.0.dist-info/METADATA +0 -5
- ras_commander-0.33.0.dist-info/RECORD +0 -14
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/top_level.txt +0 -0
ras_commander/RasHdf.py
ADDED
@@ -0,0 +1,1702 @@
|
|
1
|
+
"""
|
2
|
+
RasHdf Module
|
3
|
+
|
4
|
+
This module provides utilities for working with HDF files in HEC-RAS projects.
|
5
|
+
It contains the RasHdf class, which offers various static methods for extracting,
|
6
|
+
analyzing, and manipulating data from HEC-RAS HDF files.
|
7
|
+
|
8
|
+
Note:
|
9
|
+
This method is decorated with @hdf_operation, which handles the opening and closing of the HDF file.
|
10
|
+
The decorator should be used for all methods that directly interact with HDF files.
|
11
|
+
It ensures proper file handling and error management.
|
12
|
+
|
13
|
+
When using the @hdf_operation decorator:
|
14
|
+
- The method receives an open h5py.File object as its first argument after 'cls'.
|
15
|
+
- Error handling for file operations is managed by the decorator.
|
16
|
+
- The HDF file is automatically closed after the method execution.
|
17
|
+
|
18
|
+
Methods without this decorator must manually handle file opening, closing, and error management.
|
19
|
+
Failure to use the decorator or properly manage the file can lead to resource leaks or file access errors.
|
20
|
+
|
21
|
+
Example:
|
22
|
+
@classmethod
|
23
|
+
@hdf_operation
|
24
|
+
def example_method(cls, hdf_file: h5py.File, other_args):
|
25
|
+
# Method implementation using hdf_file
|
26
|
+
|
27
|
+
|
28
|
+
"""
|
29
|
+
import h5py
|
30
|
+
import numpy as np
|
31
|
+
import pandas as pd
|
32
|
+
from typing import Union, List, Optional, Dict, Tuple, Any, Callable
|
33
|
+
from scipy.spatial import KDTree
|
34
|
+
from pathlib import Path
|
35
|
+
from datetime import datetime
|
36
|
+
import logging
|
37
|
+
from functools import wraps
|
38
|
+
from .RasPrj import RasPrj, ras, init_ras_project
|
39
|
+
|
40
|
+
# If you're using RasPrj in type hints, you might need to use string literals to avoid circular imports
|
41
|
+
from typing import TYPE_CHECKING
|
42
|
+
if TYPE_CHECKING:
|
43
|
+
from .RasPrj import RasPrj
|
44
|
+
|
45
|
+
# Configure logging
|
46
|
+
logging.basicConfig(
|
47
|
+
level=logging.INFO,
|
48
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
49
|
+
handlers=[
|
50
|
+
logging.StreamHandler()
|
51
|
+
])
|
52
|
+
|
53
|
+
class RasHdf:
|
54
|
+
"""
|
55
|
+
A utility class for working with HDF files in HEC-RAS projects.
|
56
|
+
|
57
|
+
This class provides static methods for various operations on HDF files,
|
58
|
+
including listing paths, extracting data, and performing analyses on
|
59
|
+
HEC-RAS project data stored in HDF format.
|
60
|
+
"""
|
61
|
+
|
62
|
+
@staticmethod
|
63
|
+
def hdf_operation(func):
|
64
|
+
@wraps(func)
|
65
|
+
def wrapper(cls, hdf_input: Union[str, Path], *args: Any, **kwargs: Any) -> Any:
|
66
|
+
from ras_commander import ras # Import here to avoid circular import
|
67
|
+
ras_obj = kwargs.pop('ras_object', None) or ras
|
68
|
+
try:
|
69
|
+
hdf_filename = cls._get_hdf_filename(hdf_input, ras_obj)
|
70
|
+
with h5py.File(hdf_filename, 'r') as hdf_file:
|
71
|
+
return func(cls, hdf_file, *args, **kwargs)
|
72
|
+
except Exception as e:
|
73
|
+
logging.error(f"Error in {func.__name__}: {e}")
|
74
|
+
return None
|
75
|
+
return classmethod(wrapper)
|
76
|
+
|
77
|
+
@classmethod
|
78
|
+
def get_hdf_paths_with_properties(cls, hdf_input: Union[str, Path], ras_object=None) -> pd.DataFrame:
|
79
|
+
"""
|
80
|
+
List all paths in the HDF file with their properties.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
84
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
pd.DataFrame: DataFrame of all paths and their properties in the HDF file.
|
88
|
+
|
89
|
+
Example:
|
90
|
+
>>> paths_df = RasHdf.get_hdf_paths_with_properties("path/to/file.hdf")
|
91
|
+
>>> print(paths_df.head())
|
92
|
+
"""
|
93
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
94
|
+
paths = []
|
95
|
+
def visitor_func(name: str, node: h5py.Group) -> None:
|
96
|
+
path_info = {
|
97
|
+
"HDF_Path": name,
|
98
|
+
"Type": type(node).__name__,
|
99
|
+
"Shape": getattr(node, "shape", None),
|
100
|
+
"Size": getattr(node, "size", None),
|
101
|
+
"Dtype": getattr(node, "dtype", None)
|
102
|
+
}
|
103
|
+
paths.append(path_info)
|
104
|
+
hdf_file.visititems(visitor_func)
|
105
|
+
return pd.DataFrame(paths)
|
106
|
+
|
107
|
+
@classmethod
|
108
|
+
def get_runtime_data(cls, hdf_input: Union[str, Path], ras_object=None) -> Optional[pd.DataFrame]:
|
109
|
+
"""
|
110
|
+
Extract runtime and compute time data from a single HDF file.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
114
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Optional[pd.DataFrame]: DataFrame containing runtime and compute time data, or None if data extraction fails.
|
118
|
+
|
119
|
+
Example:
|
120
|
+
>>> runtime_df = RasHdf.get_runtime_data("path/to/file.hdf")
|
121
|
+
>>> if runtime_df is not None:
|
122
|
+
... print(runtime_df.head())
|
123
|
+
"""
|
124
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
125
|
+
logging.info(f"Extracting Plan Information from: {Path(hdf_file.filename).name}")
|
126
|
+
plan_info = hdf_file.get('/Plan Data/Plan Information')
|
127
|
+
if plan_info is None:
|
128
|
+
logging.warning("Group '/Plan Data/Plan Information' not found.")
|
129
|
+
return None
|
130
|
+
|
131
|
+
plan_name = plan_info.attrs.get('Plan Name', 'Unknown')
|
132
|
+
plan_name = plan_name.decode('utf-8') if isinstance(plan_name, bytes) else plan_name
|
133
|
+
logging.info(f"Plan Name: {plan_name}")
|
134
|
+
|
135
|
+
start_time_str = plan_info.attrs.get('Simulation Start Time', 'Unknown')
|
136
|
+
end_time_str = plan_info.attrs.get('Simulation End Time', 'Unknown')
|
137
|
+
start_time_str = start_time_str.decode('utf-8') if isinstance(start_time_str, bytes) else start_time_str
|
138
|
+
end_time_str = end_time_str.decode('utf-8') if isinstance(end_time_str, bytes) else end_time_str
|
139
|
+
|
140
|
+
start_time = datetime.strptime(start_time_str, "%d%b%Y %H:%M:%S")
|
141
|
+
end_time = datetime.strptime(end_time_str, "%d%b%Y %H:%M:%S")
|
142
|
+
simulation_duration = end_time - start_time
|
143
|
+
simulation_hours = simulation_duration.total_seconds() / 3600
|
144
|
+
|
145
|
+
logging.info(f"Simulation Start Time: {start_time_str}")
|
146
|
+
logging.info(f"Simulation End Time: {end_time_str}")
|
147
|
+
logging.info(f"Simulation Duration (hours): {simulation_hours}")
|
148
|
+
|
149
|
+
compute_processes = hdf_file.get('/Results/Summary/Compute Processes')
|
150
|
+
if compute_processes is None:
|
151
|
+
logging.warning("Dataset '/Results/Summary/Compute Processes' not found.")
|
152
|
+
return None
|
153
|
+
|
154
|
+
process_names = [name.decode('utf-8') for name in compute_processes['Process'][:]]
|
155
|
+
filenames = [filename.decode('utf-8') for filename in compute_processes['Filename'][:]]
|
156
|
+
completion_times = compute_processes['Compute Time (ms)'][:]
|
157
|
+
|
158
|
+
compute_processes_df = pd.DataFrame({
|
159
|
+
'Process': process_names,
|
160
|
+
'Filename': filenames,
|
161
|
+
'Compute Time (ms)': completion_times,
|
162
|
+
'Compute Time (s)': completion_times / 1000,
|
163
|
+
'Compute Time (hours)': completion_times / (1000 * 3600)
|
164
|
+
})
|
165
|
+
|
166
|
+
logging.debug("Compute processes DataFrame:")
|
167
|
+
logging.debug(compute_processes_df)
|
168
|
+
|
169
|
+
compute_processes_summary = {
|
170
|
+
'Plan Name': [plan_name],
|
171
|
+
'File Name': [Path(hdf_file.filename).name],
|
172
|
+
'Simulation Start Time': [start_time_str],
|
173
|
+
'Simulation End Time': [end_time_str],
|
174
|
+
'Simulation Duration (s)': [simulation_duration.total_seconds()],
|
175
|
+
'Simulation Time (hr)': [simulation_hours],
|
176
|
+
'Completing Geometry (hr)': [compute_processes_df[compute_processes_df['Process'] == 'Completing Geometry']['Compute Time (hours)'].values[0] if 'Completing Geometry' in compute_processes_df['Process'].values else 'N/A'],
|
177
|
+
'Preprocessing Geometry (hr)': [compute_processes_df[compute_processes_df['Process'] == 'Preprocessing Geometry']['Compute Time (hours)'].values[0] if 'Preprocessing Geometry' in compute_processes_df['Process'].values else 'N/A'],
|
178
|
+
'Completing Event Conditions (hr)': [compute_processes_df[compute_processes_df['Process'] == 'Completing Event Conditions']['Compute Time (hours)'].values[0] if 'Completing Event Conditions' in compute_processes_df['Process'].values else 'N/A'],
|
179
|
+
'Unsteady Flow Computations (hr)': [compute_processes_df[compute_processes_df['Process'] == 'Unsteady Flow Computations']['Compute Time (hours)'].values[0] if 'Unsteady Flow Computations' in compute_processes_df['Process'].values else 'N/A'],
|
180
|
+
'Complete Process (hr)': [compute_processes_df['Compute Time (hours)'].sum()]
|
181
|
+
}
|
182
|
+
|
183
|
+
compute_processes_summary['Unsteady Flow Speed (hr/hr)'] = [simulation_hours / compute_processes_summary['Unsteady Flow Computations (hr)'][0] if compute_processes_summary['Unsteady Flow Computations (hr)'][0] != 'N/A' else 'N/A']
|
184
|
+
compute_processes_summary['Complete Process Speed (hr/hr)'] = [simulation_hours / compute_processes_summary['Complete Process (hr)'][0] if compute_processes_summary['Complete Process (hr)'][0] != 'N/A' else 'N/A']
|
185
|
+
|
186
|
+
compute_summary_df = pd.DataFrame(compute_processes_summary)
|
187
|
+
logging.debug("Compute summary DataFrame:")
|
188
|
+
logging.debug(compute_summary_df)
|
189
|
+
|
190
|
+
return compute_summary_df
|
191
|
+
|
192
|
+
# List 2D Flow Area Groups (needed for later functions that extract specific datasets)
|
193
|
+
|
194
|
+
@classmethod
|
195
|
+
@hdf_operation
|
196
|
+
def get_2d_flow_area_names(cls, hdf_input: Union[str, Path], ras_object=None) -> Optional[List[str]]:
|
197
|
+
"""
|
198
|
+
List 2D Flow Area names from the HDF file.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
202
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Optional[List[str]]: List of 2D Flow Area names, or None if no 2D Flow Areas are found.
|
206
|
+
|
207
|
+
Raises:
|
208
|
+
ValueError: If no 2D Flow Areas are found in the HDF file.
|
209
|
+
"""
|
210
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
211
|
+
if 'Geometry/2D Flow Areas' in hdf_file:
|
212
|
+
group = hdf_file['Geometry/2D Flow Areas']
|
213
|
+
group_names = [name for name in group.keys() if isinstance(group[name], h5py.Group)]
|
214
|
+
if not group_names:
|
215
|
+
logging.warning("No 2D Flow Areas found in the HDF file")
|
216
|
+
return None
|
217
|
+
logging.info(f"Found {len(group_names)} 2D Flow Areas")
|
218
|
+
return group_names
|
219
|
+
else:
|
220
|
+
logging.warning("No 2D Flow Areas found in the HDF file")
|
221
|
+
return None
|
222
|
+
|
223
|
+
@classmethod
|
224
|
+
@hdf_operation
|
225
|
+
def get_2d_flow_area_attributes(cls, hdf_input: Union[str, Path], ras_object=None) -> Optional[pd.DataFrame]:
|
226
|
+
"""
|
227
|
+
Extract 2D Flow Area Attributes from the HDF file.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
231
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Optional[pd.DataFrame]: DataFrame containing 2D Flow Area Attributes, or None if attributes are not found.
|
235
|
+
|
236
|
+
Example:
|
237
|
+
>>> attributes_df = RasHdf.get_2d_flow_area_attributes("path/to/file.hdf")
|
238
|
+
>>> if attributes_df is not None:
|
239
|
+
... print(attributes_df.head())
|
240
|
+
... else:
|
241
|
+
... print("No 2D Flow Area attributes found")
|
242
|
+
"""
|
243
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
244
|
+
if 'Geometry/2D Flow Areas/Attributes' in hdf_file:
|
245
|
+
attributes = hdf_file['Geometry/2D Flow Areas/Attributes'][()]
|
246
|
+
attributes_df = pd.DataFrame(attributes)
|
247
|
+
logging.info(f"Extracted 2D Flow Area attributes: {attributes_df.shape[0]} rows, {attributes_df.shape[1]} columns")
|
248
|
+
return attributes_df
|
249
|
+
else:
|
250
|
+
logging.warning("No 2D Flow Area attributes found in the HDF file")
|
251
|
+
return None
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
@hdf_operation
|
255
|
+
def get_cell_info(cls, hdf_input: Union[str, Path], ras_object=None) -> Optional[pd.DataFrame]:
|
256
|
+
"""
|
257
|
+
Extract Cell Info from the HDF file.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
261
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Optional[pd.DataFrame]: DataFrame containing Cell Info, or None if the data is not found.
|
265
|
+
|
266
|
+
Example:
|
267
|
+
>>> cell_info_df = RasHdf.get_cell_info("path/to/file.hdf")
|
268
|
+
>>> if cell_info_df is not None:
|
269
|
+
... print(cell_info_df.head())
|
270
|
+
... else:
|
271
|
+
... print("No Cell Info found")
|
272
|
+
"""
|
273
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
274
|
+
cell_info_df = cls._extract_dataset(hdf_file, 'Geometry/2D Flow Areas/Cell Info', ['Start', 'End'])
|
275
|
+
if cell_info_df is not None:
|
276
|
+
logging.info(f"Extracted Cell Info: {cell_info_df.shape[0]} rows, {cell_info_df.shape[1]} columns")
|
277
|
+
else:
|
278
|
+
logging.warning("No Cell Info found in the HDF file")
|
279
|
+
return cell_info_df
|
280
|
+
|
281
|
+
@classmethod
|
282
|
+
@hdf_operation
|
283
|
+
def get_cell_points(cls, hdf_input: Union[str, Path], ras_object=None) -> Optional[pd.DataFrame]:
|
284
|
+
"""
|
285
|
+
Extract Cell Points from the HDF file.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
289
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
Optional[pd.DataFrame]: DataFrame containing Cell Points, or None if the data is not found.
|
293
|
+
|
294
|
+
Example:
|
295
|
+
>>> cell_points_df = RasHdf.get_cell_points("path/to/file.hdf")
|
296
|
+
>>> if cell_points_df is not None:
|
297
|
+
... print(cell_points_df.head())
|
298
|
+
... else:
|
299
|
+
... print("No Cell Points found")
|
300
|
+
"""
|
301
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
302
|
+
cell_points_df = cls._extract_dataset(hdf_file, 'Geometry/2D Flow Areas/Cell Points', ['X', 'Y'])
|
303
|
+
if cell_points_df is not None:
|
304
|
+
logging.info(f"Extracted Cell Points: {cell_points_df.shape[0]} rows, {cell_points_df.shape[1]} columns")
|
305
|
+
else:
|
306
|
+
logging.warning("No Cell Points found in the HDF file")
|
307
|
+
return cell_points_df
|
308
|
+
|
309
|
+
@classmethod
|
310
|
+
@hdf_operation
|
311
|
+
def get_polygon_info_and_parts(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
312
|
+
"""
|
313
|
+
Extract Polygon Info and Parts from the HDF file.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
317
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
318
|
+
If None, uses the first 2D Area Name found.
|
319
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
323
|
+
Two DataFrames containing Polygon Info and Polygon Parts respectively,
|
324
|
+
or None for each if the corresponding data is not found.
|
325
|
+
|
326
|
+
Example:
|
327
|
+
>>> polygon_info_df, polygon_parts_df = RasHdf.get_polygon_info_and_parts("path/to/file.hdf")
|
328
|
+
>>> if polygon_info_df is not None and polygon_parts_df is not None:
|
329
|
+
... print("Polygon Info:")
|
330
|
+
... print(polygon_info_df.head())
|
331
|
+
... print("Polygon Parts:")
|
332
|
+
... print(polygon_parts_df.head())
|
333
|
+
... else:
|
334
|
+
... print("Polygon data not found")
|
335
|
+
"""
|
336
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
337
|
+
# Retrieve the area name, defaulting to the first found if not provided
|
338
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
339
|
+
|
340
|
+
# Construct the base path for dataset extraction
|
341
|
+
base_path = f'Geometry/2D Flow Areas'
|
342
|
+
|
343
|
+
# Extract Polygon Info and Parts datasets
|
344
|
+
polygon_info_df = cls._extract_dataset(hdf_file, f'{base_path}/Polygon Info', ['Column1', 'Column2', 'Column3', 'Column4'])
|
345
|
+
polygon_parts_df = cls._extract_dataset(hdf_file, f'{base_path}/Polygon Parts', ['Start', 'Count'])
|
346
|
+
|
347
|
+
# Log warnings if no data is found
|
348
|
+
if polygon_info_df is None and polygon_parts_df is None:
|
349
|
+
logging.warning(f"No Polygon Info or Parts found for 2D Flow Area: {area_name}")
|
350
|
+
|
351
|
+
return polygon_info_df, polygon_parts_df
|
352
|
+
|
353
|
+
@classmethod
|
354
|
+
@hdf_operation
|
355
|
+
def get_polygon_points(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[pd.DataFrame]:
|
356
|
+
"""
|
357
|
+
Extract Polygon Points from the HDF file.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
361
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
362
|
+
If None, uses the first 2D Area Name found.
|
363
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
Optional[pd.DataFrame]: DataFrame containing Polygon Points, or None if the data is not found.
|
367
|
+
"""
|
368
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
369
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
370
|
+
# This path does not include the area name
|
371
|
+
polygon_points_path = f'Geometry/2D Flow Areas/Polygon Points'
|
372
|
+
if polygon_points_path in hdf_file:
|
373
|
+
polygon_points = hdf_file[polygon_points_path][()]
|
374
|
+
polygon_points_df = pd.DataFrame(polygon_points, columns=['X', 'Y'])
|
375
|
+
logging.info(f"Extracted Polygon Points for 2D Flow Area {area_name}: {polygon_points_df.shape[0]} rows, {polygon_points_df.shape[1]} columns")
|
376
|
+
return polygon_points_df
|
377
|
+
else:
|
378
|
+
logging.warning(f"No Polygon Points found for 2D Flow Area: {area_name}")
|
379
|
+
return None
|
380
|
+
|
381
|
+
@classmethod
|
382
|
+
@hdf_operation
|
383
|
+
def get_cells_center_data(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
384
|
+
"""
|
385
|
+
Extract Cells Center Coordinates and Manning's n from the HDF file.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
389
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
390
|
+
If None, uses the first 2D Area Name found.
|
391
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
392
|
+
|
393
|
+
Returns:
|
394
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
395
|
+
Two DataFrames containing Cells Center Coordinates and Manning's n respectively,
|
396
|
+
or None for each if the corresponding data is not found.
|
397
|
+
|
398
|
+
Example:
|
399
|
+
>>> coords_df, mannings_df = RasHdf.get_cells_center_data("path/to/file.hdf")
|
400
|
+
>>> if coords_df is not None and mannings_df is not None:
|
401
|
+
... print("Cell Center Coordinates:")
|
402
|
+
... print(coords_df.head())
|
403
|
+
... print("Manning's n:")
|
404
|
+
... print(mannings_df.head())
|
405
|
+
... else:
|
406
|
+
... print("Cell center data not found")
|
407
|
+
"""
|
408
|
+
logging.info(f"Entering get_cells_center_data method")
|
409
|
+
logging.info(f"Input parameters: hdf_input={hdf_input}, area_name={area_name}")
|
410
|
+
|
411
|
+
try:
|
412
|
+
hdf_filename = cls._get_hdf_filename(hdf_input, ras_object)
|
413
|
+
logging.info(f"HDF filename: {hdf_filename}")
|
414
|
+
|
415
|
+
with h5py.File(hdf_filename, 'r') as hdf_file:
|
416
|
+
logging.info(f"Successfully opened HDF file: {hdf_filename}")
|
417
|
+
|
418
|
+
logging.info(f"Getting Cells Center Data for 2D Flow Area: {area_name}")
|
419
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
420
|
+
logging.info(f"Area Name: {area_name}")
|
421
|
+
|
422
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
423
|
+
cells_center_coord_path = f'{base_path}/Cells Center Coordinate'
|
424
|
+
cells_manning_n_path = f'{base_path}/Cells Center Manning\'s n'
|
425
|
+
|
426
|
+
logging.info(f"Extracting dataset from path: {cells_center_coord_path}")
|
427
|
+
cells_center_coord_df = cls._extract_dataset(hdf_file, cells_center_coord_path, ['X', 'Y'])
|
428
|
+
|
429
|
+
logging.info(f"Extracting dataset from path: {cells_manning_n_path}")
|
430
|
+
cells_manning_n_df = cls._extract_dataset(hdf_file, cells_manning_n_path, ['Manning\'s n'])
|
431
|
+
|
432
|
+
if cells_center_coord_df is not None and cells_manning_n_df is not None:
|
433
|
+
logging.info(f"Extracted Cells Center Data for 2D Flow Area: {area_name}")
|
434
|
+
logging.info(f"Cells Center Coordinates shape: {cells_center_coord_df.shape}, dtype: {cells_center_coord_df.dtypes}")
|
435
|
+
logging.info(f"Cells Manning's n shape: {cells_manning_n_df.shape}, dtype: {cells_manning_n_df.dtypes}")
|
436
|
+
else:
|
437
|
+
logging.warning(f"Cells Center Data not found for 2D Flow Area: {area_name}")
|
438
|
+
|
439
|
+
return cells_center_coord_df, cells_manning_n_df
|
440
|
+
|
441
|
+
except Exception as e:
|
442
|
+
logging.error(f"Error in get_cells_center_data: {str(e)}", exc_info=True)
|
443
|
+
return None, None
|
444
|
+
|
445
|
+
@classmethod
|
446
|
+
@hdf_operation
|
447
|
+
def get_faces_area_elevation_data(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[pd.DataFrame]:
|
448
|
+
"""
|
449
|
+
Extract Faces Area Elevation Values from the HDF file.
|
450
|
+
|
451
|
+
Args:
|
452
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
453
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
454
|
+
If None, uses the first 2D Area Name found.
|
455
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
456
|
+
|
457
|
+
Returns:
|
458
|
+
Optional[pd.DataFrame]: DataFrame containing Faces Area Elevation Values, or None if the data is not found.
|
459
|
+
|
460
|
+
Example:
|
461
|
+
>>> elevation_df = RasHdf.get_faces_area_elevation_data("path/to/file.hdf")
|
462
|
+
>>> if elevation_df is not None:
|
463
|
+
... print(elevation_df.head())
|
464
|
+
... else:
|
465
|
+
... print("No Faces Area Elevation data found")
|
466
|
+
"""
|
467
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
468
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
469
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
470
|
+
area_elev_values_path = f'{base_path}/Faces Area Elevation Values'
|
471
|
+
|
472
|
+
if area_elev_values_path in hdf_file:
|
473
|
+
area_elev_values = hdf_file[area_elev_values_path][()]
|
474
|
+
area_elev_values_df = pd.DataFrame(area_elev_values, columns=['Elevation', 'Area', 'Wetted Perimeter', 'Manning\'s n'])
|
475
|
+
|
476
|
+
logging.info(f"Extracted Faces Area Elevation Values for 2D Flow Area: {area_name}")
|
477
|
+
logging.info(f"Faces Area Elevation Values shape: {area_elev_values.shape}, dtype: {area_elev_values.dtype}")
|
478
|
+
|
479
|
+
return area_elev_values_df
|
480
|
+
else:
|
481
|
+
logging.warning(f"Faces Area Elevation Values not found for 2D Flow Area: {area_name}")
|
482
|
+
return None
|
483
|
+
|
484
|
+
@classmethod
|
485
|
+
@hdf_operation
|
486
|
+
def get_faces_indexes(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
487
|
+
"""
|
488
|
+
Extract Faces Cell and FacePoint Indexes from the HDF file.
|
489
|
+
|
490
|
+
Args:
|
491
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
492
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
493
|
+
If None, uses the first 2D Area Name found.
|
494
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
495
|
+
|
496
|
+
Returns:
|
497
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
498
|
+
Two DataFrames containing Faces Cell Indexes and FacePoint Indexes respectively,
|
499
|
+
or None for each if the corresponding data is not found.
|
500
|
+
|
501
|
+
Example:
|
502
|
+
>>> cell_indexes_df, facepoint_indexes_df = RasHdf.get_faces_indexes("path/to/file.hdf")
|
503
|
+
>>> if cell_indexes_df is not None and facepoint_indexes_df is not None:
|
504
|
+
... print("Faces Cell Indexes:")
|
505
|
+
... print(cell_indexes_df.head())
|
506
|
+
... print("Faces FacePoint Indexes:")
|
507
|
+
... print(facepoint_indexes_df.head())
|
508
|
+
... else:
|
509
|
+
... print("Faces indexes data not found")
|
510
|
+
"""
|
511
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
512
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
513
|
+
|
514
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
515
|
+
cell_indexes_path = f'{base_path}/Faces Cell Indexes'
|
516
|
+
facepoint_indexes_path = f'{base_path}/Faces FacePoint Indexes'
|
517
|
+
|
518
|
+
cell_indexes_df = cls._extract_dataset(hdf_file, cell_indexes_path, ['Left Cell', 'Right Cell'])
|
519
|
+
facepoint_indexes_df = cls._extract_dataset(hdf_file, facepoint_indexes_path, ['Start FacePoint', 'End FacePoint'])
|
520
|
+
|
521
|
+
if cell_indexes_df is not None and facepoint_indexes_df is not None:
|
522
|
+
logging.info(f"Extracted Faces Indexes for 2D Flow Area: {area_name}")
|
523
|
+
else:
|
524
|
+
logging.warning(f"Faces Indexes not found for 2D Flow Area: {area_name}")
|
525
|
+
|
526
|
+
return cell_indexes_df, facepoint_indexes_df
|
527
|
+
|
528
|
+
|
529
|
+
|
530
|
+
@classmethod
|
531
|
+
@hdf_operation
|
532
|
+
def get_faces_elevation_data(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
533
|
+
"""
|
534
|
+
Extract Faces Low Elevation Centroid and Minimum Elevation from the HDF file.
|
535
|
+
|
536
|
+
Args:
|
537
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
538
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
539
|
+
If None, uses the first 2D Area Name found.
|
540
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
541
|
+
|
542
|
+
Returns:
|
543
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
544
|
+
DataFrames containing Faces Low Elevation Centroid and Minimum Elevation.
|
545
|
+
"""
|
546
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
547
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
548
|
+
|
549
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
550
|
+
low_elev_centroid = cls._extract_dataset(hdf_file, f'{base_path}/Faces Low Elevation Centroid', ['Low Elevation Centroid'])
|
551
|
+
min_elevation = cls._extract_dataset(hdf_file, f'{base_path}/Faces Minimum Elevation', ['Minimum Elevation'])
|
552
|
+
|
553
|
+
if low_elev_centroid is not None and min_elevation is not None:
|
554
|
+
logging.info(f"Extracted Faces Elevation Data for 2D Flow Area: {area_name}")
|
555
|
+
else:
|
556
|
+
logging.warning(f"Faces Elevation Data not found for 2D Flow Area: {area_name}")
|
557
|
+
|
558
|
+
return low_elev_centroid, min_elevation
|
559
|
+
|
560
|
+
@classmethod
|
561
|
+
@hdf_operation
|
562
|
+
def get_faces_vector_data(
|
563
|
+
cls,
|
564
|
+
hdf_input: Union[str, Path],
|
565
|
+
area_name: Optional[str] = None,
|
566
|
+
ras_object=None
|
567
|
+
) -> Optional[pd.DataFrame]:
|
568
|
+
"""
|
569
|
+
Extract Faces NormalUnitVector and Length from the HDF file.
|
570
|
+
|
571
|
+
Args:
|
572
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
573
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
574
|
+
If None, uses the first 2D Area Name found.
|
575
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
Optional[pd.DataFrame]: DataFrame containing Faces NormalUnitVector and Length.
|
579
|
+
"""
|
580
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
581
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
582
|
+
|
583
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
584
|
+
vector_data = cls._extract_dataset(hdf_file, f'{base_path}/Faces NormalUnitVector and Length', ['NormalX', 'NormalY', 'Length'])
|
585
|
+
|
586
|
+
if vector_data is not None:
|
587
|
+
logging.info(f"Extracted Faces Vector Data for 2D Flow Area: {area_name}")
|
588
|
+
else:
|
589
|
+
logging.warning(f"Faces Vector Data not found for 2D Flow Area: {area_name}")
|
590
|
+
|
591
|
+
return vector_data
|
592
|
+
|
593
|
+
@classmethod
|
594
|
+
@hdf_operation
|
595
|
+
def get_faces_perimeter_data(
|
596
|
+
cls,
|
597
|
+
hdf_input: Union[str, Path],
|
598
|
+
area_name: Optional[str] = None,
|
599
|
+
ras_object=None
|
600
|
+
) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
601
|
+
"""
|
602
|
+
Extract Faces Perimeter Info and Values from the HDF file.
|
603
|
+
|
604
|
+
Args:
|
605
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
606
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
607
|
+
If None, uses the first 2D Area Name found.
|
608
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
609
|
+
|
610
|
+
Returns:
|
611
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
612
|
+
DataFrames containing Faces Perimeter Info and Values.
|
613
|
+
|
614
|
+
Raises:
|
615
|
+
ValueError: If no HDF file is found for the given plan number.
|
616
|
+
FileNotFoundError: If the specified HDF file does not exist.
|
617
|
+
|
618
|
+
Example:
|
619
|
+
>>> perimeter_info_df, perimeter_values_df = RasHdf.get_faces_perimeter_data("path/to/file.hdf")
|
620
|
+
>>> if perimeter_info_df is not None and perimeter_values_df is not None:
|
621
|
+
... print("Perimeter Info:")
|
622
|
+
... print(perimeter_info_df.head())
|
623
|
+
... print("Perimeter Values:")
|
624
|
+
... print(perimeter_values_df.head())
|
625
|
+
... else:
|
626
|
+
... print("Perimeter data not found")
|
627
|
+
"""
|
628
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
629
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
630
|
+
|
631
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}'
|
632
|
+
perimeter_info = cls._extract_dataset(hdf_file, f'{base_path}/Faces Perimeter Info', ['Start', 'Count'])
|
633
|
+
perimeter_values = cls._extract_dataset(hdf_file, f'{base_path}/Faces Perimeter Values', ['X', 'Y'])
|
634
|
+
|
635
|
+
if perimeter_info is not None and perimeter_values is not None:
|
636
|
+
logging.info(f"Extracted Faces Perimeter Data for 2D Flow Area: {area_name}")
|
637
|
+
else:
|
638
|
+
logging.warning(f"Faces Perimeter Data not found for 2D Flow Area: {area_name}")
|
639
|
+
|
640
|
+
return perimeter_info, perimeter_values
|
641
|
+
|
642
|
+
@classmethod
|
643
|
+
@hdf_operation
|
644
|
+
def get_infiltration_data(
|
645
|
+
cls,
|
646
|
+
hdf_input: Union[str, Path],
|
647
|
+
area_name: Optional[str] = None,
|
648
|
+
ras_object=None
|
649
|
+
) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
650
|
+
"""
|
651
|
+
Extract Infiltration Data from the HDF file.
|
652
|
+
|
653
|
+
Args:
|
654
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
655
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
656
|
+
If None, uses the first 2D Area Name found.
|
657
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
658
|
+
|
659
|
+
Returns:
|
660
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
661
|
+
DataFrames containing various Infiltration Data
|
662
|
+
"""
|
663
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
664
|
+
# Retrieve the area name from the HDF file or use the first found
|
665
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
666
|
+
|
667
|
+
# Define the base path for the Infiltration data
|
668
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}/Infiltration'
|
669
|
+
|
670
|
+
# Extract various datasets related to infiltration
|
671
|
+
cell_classifications = cls._extract_dataset(hdf_file, f'{base_path}/Cell Center Classifications', ['Cell Classification'])
|
672
|
+
face_classifications = cls._extract_dataset(hdf_file, f'{base_path}/Face Center Classifications', ['Face Classification'])
|
673
|
+
initial_deficit = cls._extract_dataset(hdf_file, f'{base_path}/Initial Deficit', ['Initial Deficit'])
|
674
|
+
maximum_deficit = cls._extract_dataset(hdf_file, f'{base_path}/Maximum Deficit', ['Maximum Deficit'])
|
675
|
+
potential_percolation_rate = cls._extract_dataset(hdf_file, f'{base_path}/Potential Percolation Rate', ['Potential Percolation Rate'])
|
676
|
+
|
677
|
+
# Log the extraction status
|
678
|
+
if all(df is not None for df in [cell_classifications, face_classifications, initial_deficit, maximum_deficit, potential_percolation_rate]):
|
679
|
+
logging.info(f"Extracted Infiltration Data for 2D Flow Area: {area_name}")
|
680
|
+
else:
|
681
|
+
logging.warning(f"Some or all Infiltration Data not found for 2D Flow Area: {area_name}")
|
682
|
+
|
683
|
+
return cell_classifications, face_classifications, initial_deficit, maximum_deficit, potential_percolation_rate
|
684
|
+
|
685
|
+
@classmethod
|
686
|
+
@hdf_operation
|
687
|
+
def get_percent_impervious_data(
|
688
|
+
cls,
|
689
|
+
hdf_input: Union[str, Path],
|
690
|
+
area_name: Optional[str] = None,
|
691
|
+
ras_object=None
|
692
|
+
) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
693
|
+
"""
|
694
|
+
Extract Percent Impervious Data from the HDF file.
|
695
|
+
|
696
|
+
Args:
|
697
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
698
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
699
|
+
If None, uses the first 2D Area Name found.
|
700
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
701
|
+
|
702
|
+
Returns:
|
703
|
+
Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame], Optional[pd.DataFrame]]:
|
704
|
+
DataFrames containing Cell Classifications, Face Classifications, and Percent Impervious Data
|
705
|
+
"""
|
706
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
707
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
708
|
+
|
709
|
+
base_path = f'Geometry/2D Flow Areas/{area_name}/Percent Impervious'
|
710
|
+
cell_classifications = cls._extract_dataset(hdf_file, f'{base_path}/Cell Center Classifications', ['Cell Classification'])
|
711
|
+
face_classifications = cls._extract_dataset(hdf_file, f'{base_path}/Face Center Classifications', ['Face Classification'])
|
712
|
+
percent_impervious = cls._extract_dataset(hdf_file, f'{base_path}/Percent Impervious', ['Percent Impervious'])
|
713
|
+
|
714
|
+
if all([df is not None for df in [cell_classifications, face_classifications, percent_impervious]]):
|
715
|
+
logging.info(f"Extracted Percent Impervious Data for 2D Flow Area: {area_name}")
|
716
|
+
else:
|
717
|
+
logging.warning(f"Some or all Percent Impervious Data not found for 2D Flow Area: {area_name}")
|
718
|
+
|
719
|
+
return cell_classifications, face_classifications, percent_impervious
|
720
|
+
|
721
|
+
@classmethod
|
722
|
+
@hdf_operation
|
723
|
+
def get_perimeter_data(
|
724
|
+
cls,
|
725
|
+
hdf_input: Union[str, Path],
|
726
|
+
area_name: Optional[str] = None,
|
727
|
+
ras_object=None
|
728
|
+
) -> Optional[pd.DataFrame]:
|
729
|
+
"""
|
730
|
+
Extract Perimeter Data from the HDF file.
|
731
|
+
|
732
|
+
Args:
|
733
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
734
|
+
area_name (Optional[str]): Name of the 2D Flow Area to extract data from.
|
735
|
+
If None, uses the first 2D Area Name found.
|
736
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
737
|
+
|
738
|
+
Returns:
|
739
|
+
Optional[pd.DataFrame]: DataFrame containing Perimeter Data
|
740
|
+
|
741
|
+
Example:
|
742
|
+
>>> perimeter_df = RasHdf.get_perimeter_data("path/to/file.hdf")
|
743
|
+
>>> if perimeter_df is not None:
|
744
|
+
... print(perimeter_df.head())
|
745
|
+
... else:
|
746
|
+
... print("Perimeter data not found")
|
747
|
+
"""
|
748
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
749
|
+
area_name = cls._get_area_name(hdf_file, area_name, hdf_file.filename)
|
750
|
+
|
751
|
+
perimeter_path = f'Geometry/2D Flow Areas/{area_name}/Perimeter'
|
752
|
+
perimeter_df = cls._extract_dataset(hdf_file, perimeter_path, ['X', 'Y'])
|
753
|
+
|
754
|
+
if perimeter_df is not None:
|
755
|
+
logging.info(f"Extracted Perimeter Data for 2D Flow Area: {area_name}")
|
756
|
+
else:
|
757
|
+
logging.warning(f"Perimeter Data not found for 2D Flow Area: {area_name}")
|
758
|
+
|
759
|
+
return perimeter_df
|
760
|
+
|
761
|
+
# Private Class Methods (to save code duplication)
|
762
|
+
|
763
|
+
|
764
|
+
@classmethod
|
765
|
+
def _get_area_name(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> str:
|
766
|
+
"""
|
767
|
+
Get the 2D Flow Area name from the HDF file.
|
768
|
+
|
769
|
+
Args:
|
770
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
771
|
+
area_name (Optional[str]): The provided area name, if any.
|
772
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
773
|
+
|
774
|
+
Returns:
|
775
|
+
str: The 2D Flow Area name.
|
776
|
+
|
777
|
+
Raises:
|
778
|
+
ValueError: If no 2D Flow Areas are found in the HDF file or if the specified area name is not found.
|
779
|
+
"""
|
780
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
781
|
+
if area_name is None:
|
782
|
+
area_names = [name for name in hdf_file['Geometry/2D Flow Areas'].keys() if isinstance(hdf_file['Geometry/2D Flow Areas'][name], h5py.Group)]
|
783
|
+
if not area_names:
|
784
|
+
raise ValueError("No 2D Flow Areas found in the HDF file")
|
785
|
+
area_name = area_names[0]
|
786
|
+
logging.info(f"Using first 2D Flow Area found: {area_name}")
|
787
|
+
else:
|
788
|
+
if area_name not in hdf_file['Geometry/2D Flow Areas']:
|
789
|
+
raise ValueError(f"2D Flow Area '{area_name}' not found in the HDF file")
|
790
|
+
logging.info(f"Using 2D Flow Area provided by user: {area_name}")
|
791
|
+
return area_name
|
792
|
+
|
793
|
+
@classmethod
|
794
|
+
def _extract_dataset(cls, hdf_input: Union[str, Path], dataset_path: str, column_names: List[str], ras_object=None) -> Optional[pd.DataFrame]:
|
795
|
+
"""
|
796
|
+
Extract a dataset from the HDF file and convert it to a DataFrame.
|
797
|
+
|
798
|
+
Args:
|
799
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
800
|
+
dataset_path (str): The path to the dataset within the HDF file.
|
801
|
+
column_names (List[str]): The names to assign to the DataFrame columns.
|
802
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
803
|
+
|
804
|
+
Returns:
|
805
|
+
Optional[pd.DataFrame]: The extracted data as a DataFrame, or None if the dataset is not found.
|
806
|
+
"""
|
807
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
808
|
+
try:
|
809
|
+
dataset = hdf_file[dataset_path][()]
|
810
|
+
df = pd.DataFrame(dataset, columns=column_names)
|
811
|
+
logging.info(f"Extracted dataset: {dataset_path}")
|
812
|
+
return df
|
813
|
+
except KeyError:
|
814
|
+
logging.warning(f"Dataset not found: {dataset_path}")
|
815
|
+
return None
|
816
|
+
@classmethod
|
817
|
+
@hdf_operation
|
818
|
+
def read_hdf_to_dataframe(cls, hdf_input: Union[str, Path], dataset_path: str, fill_value: Union[int, float, str] = -9999, ras_object=None) -> pd.DataFrame:
|
819
|
+
"""
|
820
|
+
Reads an HDF5 dataset and converts it into a pandas DataFrame, handling byte strings and missing values.
|
821
|
+
|
822
|
+
Args:
|
823
|
+
hdf_input (Union[str, Path]): Path to the HDF file or plan number.
|
824
|
+
dataset_path (str): Path to the dataset within the HDF file.
|
825
|
+
fill_value (Union[int, float, str], optional): The value to use for filling missing data. Defaults to -9999.
|
826
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
827
|
+
|
828
|
+
Returns:
|
829
|
+
pd.DataFrame: The resulting DataFrame with byte strings decoded and missing values replaced.
|
830
|
+
|
831
|
+
Raises:
|
832
|
+
KeyError: If the dataset is not found in the HDF file.
|
833
|
+
"""
|
834
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
835
|
+
try:
|
836
|
+
hdf_dataset = hdf_file[dataset_path]
|
837
|
+
hdf_dataframe = cls.convert_to_dataframe_array(hdf_dataset)
|
838
|
+
byte_columns = [col for col in hdf_dataframe.columns if isinstance(hdf_dataframe[col].iloc[0], (bytes, bytearray))]
|
839
|
+
|
840
|
+
hdf_dataframe[byte_columns] = hdf_dataframe[byte_columns].applymap(lambda x: x.decode('utf-8') if isinstance(x, (bytes, bytearray)) else x)
|
841
|
+
hdf_dataframe = hdf_dataframe.replace({fill_value: np.NaN})
|
842
|
+
|
843
|
+
logging.info(f"Successfully read dataset: {dataset_path}")
|
844
|
+
return hdf_dataframe
|
845
|
+
except KeyError:
|
846
|
+
logging.error(f"Dataset not found: {dataset_path}")
|
847
|
+
raise
|
848
|
+
|
849
|
+
@classmethod
|
850
|
+
@hdf_operation
|
851
|
+
def get_group_attributes_as_df(cls, hdf_input: Union[str, Path], group_path: str, ras_object=None) -> pd.DataFrame:
|
852
|
+
"""
|
853
|
+
Convert attributes inside a given HDF group to a DataFrame.
|
854
|
+
|
855
|
+
Args:
|
856
|
+
hdf_input (Union[str, Path]): Path to the HDF file or plan number.
|
857
|
+
group_path (str): Path of the group in the HDF file.
|
858
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
859
|
+
|
860
|
+
Returns:
|
861
|
+
pd.DataFrame: DataFrame of all attributes in the specified group with their properties.
|
862
|
+
|
863
|
+
Raises:
|
864
|
+
KeyError: If the specified group_path is not found in the file.
|
865
|
+
|
866
|
+
Example:
|
867
|
+
>>> attributes_df = RasHdf.get_group_attributes_as_df("path/to/file.hdf", "/Results/Unsteady/Output")
|
868
|
+
>>> print(attributes_df.head())
|
869
|
+
"""
|
870
|
+
hdf_filename = cls._get_hdf_filename(hdf_input, ras_object)
|
871
|
+
|
872
|
+
with h5py.File(hdf_filename, 'r') as hdf_file:
|
873
|
+
try:
|
874
|
+
group = hdf_file[group_path]
|
875
|
+
attributes = []
|
876
|
+
for attr in group.attrs:
|
877
|
+
value = group.attrs[attr]
|
878
|
+
attr_info = {
|
879
|
+
'Attribute': attr,
|
880
|
+
'Value': value,
|
881
|
+
'Type': type(value).__name__,
|
882
|
+
'Shape': value.shape if isinstance(value, np.ndarray) else None,
|
883
|
+
'Size': value.size if isinstance(value, np.ndarray) else None,
|
884
|
+
'Dtype': value.dtype if isinstance(value, np.ndarray) else None
|
885
|
+
}
|
886
|
+
if isinstance(value, bytes):
|
887
|
+
attr_info['Value'] = value.decode('utf-8')
|
888
|
+
elif isinstance(value, np.ndarray):
|
889
|
+
if value.dtype.kind == 'S':
|
890
|
+
attr_info['Value'] = [v.decode('utf-8') for v in value]
|
891
|
+
elif value.dtype.kind in ['i', 'f', 'u']:
|
892
|
+
attr_info['Value'] = value.tolist()
|
893
|
+
attributes.append(attr_info)
|
894
|
+
|
895
|
+
return pd.DataFrame(attributes)
|
896
|
+
except KeyError:
|
897
|
+
logging.error(f"Group not found: {group_path}")
|
898
|
+
raise
|
899
|
+
|
900
|
+
# Last functions from PyHMT2D:
|
901
|
+
|
902
|
+
|
903
|
+
@classmethod
|
904
|
+
@hdf_operation
|
905
|
+
def get_2d_area_solution_times(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[np.ndarray]:
|
906
|
+
"""
|
907
|
+
Retrieve solution times for a specified 2D Flow Area.
|
908
|
+
|
909
|
+
Args:
|
910
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
911
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, uses the first area found.
|
912
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
913
|
+
|
914
|
+
Returns:
|
915
|
+
Optional[np.ndarray]: Array of solution times, or None if not found.
|
916
|
+
|
917
|
+
Example:
|
918
|
+
>>> solution_times = RasHdf.get_2d_area_solution_times("03", area_name="Area1")
|
919
|
+
>>> print(solution_times)
|
920
|
+
[0.0, 0.5, 1.0, ...]
|
921
|
+
"""
|
922
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
923
|
+
try:
|
924
|
+
solution_times = np.array(
|
925
|
+
hdf_file['Results']['Unsteady']['Output']['Output Blocks']
|
926
|
+
['Base Output']['Unsteady Time Series']['Time']
|
927
|
+
)
|
928
|
+
logging.info(f"Retrieved {len(solution_times)} solution times for 2D Flow Area: {area_name}")
|
929
|
+
return solution_times
|
930
|
+
except KeyError:
|
931
|
+
logging.warning(f"Solution times not found for 2D Flow Area: {area_name}")
|
932
|
+
return None
|
933
|
+
@classmethod
|
934
|
+
@hdf_operation
|
935
|
+
def get_2d_area_solution_time_dates(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[np.ndarray]:
|
936
|
+
"""
|
937
|
+
Retrieve solution time dates for a specified 2D Flow Area.
|
938
|
+
|
939
|
+
Args:
|
940
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
941
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, uses the first area found.
|
942
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
943
|
+
|
944
|
+
Returns:
|
945
|
+
Optional[np.ndarray]: Array of solution time dates, or None if not found.
|
946
|
+
|
947
|
+
Example:
|
948
|
+
>>> solution_time_dates = RasHdf.get_2d_area_solution_time_dates("03", area_name="Area1")
|
949
|
+
>>> print(solution_time_dates)
|
950
|
+
['2024-01-01T00:00:00', '2024-01-01T00:30:00', ...]
|
951
|
+
"""
|
952
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
953
|
+
try:
|
954
|
+
solution_time_dates = np.array(
|
955
|
+
hdf_file['Results']['Unsteady']['Output']['Output Blocks']
|
956
|
+
['Base Output']['Unsteady Time Series']['Time Date Stamp']
|
957
|
+
)
|
958
|
+
logging.info(f"Retrieved {len(solution_time_dates)} solution time dates for 2D Flow Area: {area_name}")
|
959
|
+
return solution_time_dates
|
960
|
+
except KeyError:
|
961
|
+
logging.warning(f"Solution time dates not found for 2D Flow Area: {area_name}")
|
962
|
+
return None
|
963
|
+
|
964
|
+
@classmethod
|
965
|
+
@hdf_operation
|
966
|
+
def load_2d_area_solutions(
|
967
|
+
cls,
|
968
|
+
hdf_file: h5py.File,
|
969
|
+
ras_object=None
|
970
|
+
) -> Optional[Dict[str, pd.DataFrame]]:
|
971
|
+
"""
|
972
|
+
Load 2D Area Solutions (Water Surface Elevation and Face Normal Velocity) from the HDF file
|
973
|
+
and provide them as pandas DataFrames.
|
974
|
+
|
975
|
+
**Note:**
|
976
|
+
- This function has only been tested with HEC-RAS version 6.5.
|
977
|
+
- Ensure that the HDF file structure matches the expected paths.
|
978
|
+
|
979
|
+
Args:
|
980
|
+
hdf_file (h5py.File): An open HDF5 file object.
|
981
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
982
|
+
|
983
|
+
Returns:
|
984
|
+
Optional[Dict[str, pd.DataFrame]]: A dictionary containing:
|
985
|
+
- 'solution_times': DataFrame of solution times.
|
986
|
+
- For each 2D Flow Area:
|
987
|
+
- '{Area_Name}_WSE': Water Surface Elevation DataFrame.
|
988
|
+
- '{Area_Name}_Face_Velocity': Face Normal Velocity DataFrame.
|
989
|
+
"""
|
990
|
+
try:
|
991
|
+
# Extract solution times
|
992
|
+
solution_times_path = '/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Time'
|
993
|
+
if solution_times_path not in hdf_file:
|
994
|
+
logging.error(f"Solution times dataset not found at path: {solution_times_path}")
|
995
|
+
return None
|
996
|
+
|
997
|
+
solution_times = hdf_file[solution_times_path][()]
|
998
|
+
solution_times_df = pd.DataFrame({
|
999
|
+
'Time_Step': solution_times
|
1000
|
+
})
|
1001
|
+
logging.info(f"Extracted Solution Times: {solution_times_df.shape[0]} time steps")
|
1002
|
+
|
1003
|
+
# Initialize dictionary to hold all dataframes
|
1004
|
+
solutions_dict = {
|
1005
|
+
'solution_times': solution_times_df
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
# Get list of 2D Flow Areas
|
1009
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1010
|
+
if not two_d_area_names:
|
1011
|
+
logging.error("No 2D Flow Areas found in the HDF file.")
|
1012
|
+
return solutions_dict
|
1013
|
+
|
1014
|
+
for area in two_d_area_names:
|
1015
|
+
logging.info(f"Processing 2D Flow Area: {area}")
|
1016
|
+
|
1017
|
+
# Paths for WSE and Face Velocity datasets
|
1018
|
+
wse_path = f'/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/2D Flow Areas/{area}/Water Surface'
|
1019
|
+
face_velocity_path = f'/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/2D Flow Areas/{area}/Face Velocity'
|
1020
|
+
|
1021
|
+
# Extract Water Surface Elevation (WSE)
|
1022
|
+
if wse_path not in hdf_file:
|
1023
|
+
logging.warning(f"WSE dataset not found for area '{area}' at path: {wse_path}")
|
1024
|
+
continue
|
1025
|
+
|
1026
|
+
wse_data = hdf_file[wse_path][()]
|
1027
|
+
# Assuming cell center coordinates are required for WSE
|
1028
|
+
cell_center_coords_path = f'/Geometry/2D Flow Areas/{area}/Cell Center Coordinate'
|
1029
|
+
if cell_center_coords_path not in hdf_file:
|
1030
|
+
logging.warning(f"Cell Center Coordinate dataset not found for area '{area}' at path: {cell_center_coords_path}")
|
1031
|
+
continue
|
1032
|
+
|
1033
|
+
cell_center_coords = hdf_file[cell_center_coords_path][()]
|
1034
|
+
if cell_center_coords.shape[0] != wse_data.shape[1]:
|
1035
|
+
logging.warning(f"Mismatch between Cell Center Coordinates and WSE data for area '{area}'.")
|
1036
|
+
continue
|
1037
|
+
|
1038
|
+
wse_df = pd.DataFrame({
|
1039
|
+
'Time_Step': np.repeat(solution_times, wse_data.shape[1]),
|
1040
|
+
'Cell_ID': np.tile(np.arange(wse_data.shape[1]), wse_data.shape[0]),
|
1041
|
+
'X': cell_center_coords[:, 0].repeat(wse_data.shape[0]),
|
1042
|
+
'Y': cell_center_coords[:, 1].repeat(wse_data.shape[0]),
|
1043
|
+
'WSE': wse_data.flatten()
|
1044
|
+
})
|
1045
|
+
solutions_dict[f'{area}_WSE'] = wse_df
|
1046
|
+
logging.info(f"Extracted WSE for area '{area}': {wse_df.shape[0]} records")
|
1047
|
+
|
1048
|
+
# Extract Face Normal Velocity
|
1049
|
+
if face_velocity_path not in hdf_file:
|
1050
|
+
logging.warning(f"Face Velocity dataset not found for area '{area}' at path: {face_velocity_path}")
|
1051
|
+
continue
|
1052
|
+
|
1053
|
+
face_velocity_data = hdf_file[face_velocity_path][()]
|
1054
|
+
# Assuming face center points are required for velocities
|
1055
|
+
face_center_coords_path = f'/Geometry/2D Flow Areas/{area}/Face Points Coordinates'
|
1056
|
+
if face_center_coords_path not in hdf_file:
|
1057
|
+
logging.warning(f"Face Points Coordinates dataset not found for area '{area}' at path: {face_center_coords_path}")
|
1058
|
+
continue
|
1059
|
+
|
1060
|
+
face_center_coords = hdf_file[face_center_coords_path][()]
|
1061
|
+
if face_center_coords.shape[0] != face_velocity_data.shape[1]:
|
1062
|
+
logging.warning(f"Mismatch between Face Center Coordinates and Face Velocity data for area '{area}'.")
|
1063
|
+
continue
|
1064
|
+
|
1065
|
+
face_velocity_df = pd.DataFrame({
|
1066
|
+
'Time_Step': np.repeat(solution_times, face_velocity_data.shape[1]),
|
1067
|
+
'Face_ID': np.tile(np.arange(face_velocity_data.shape[1]), face_velocity_data.shape[0]),
|
1068
|
+
'X': face_center_coords[:, 0].repeat(face_velocity_data.shape[0]),
|
1069
|
+
'Y': face_center_coords[:, 1].repeat(face_velocity_data.shape[0]),
|
1070
|
+
'Normal_Velocity_ft_s': face_velocity_data.flatten()
|
1071
|
+
})
|
1072
|
+
solutions_dict[f'{area}_Face_Velocity'] = face_velocity_df
|
1073
|
+
logging.info(f"Extracted Face Velocity for area '{area}': {face_velocity_df.shape[0]} records")
|
1074
|
+
|
1075
|
+
return solutions_dict
|
1076
|
+
|
1077
|
+
except Exception as e:
|
1078
|
+
logging.error(f"An error occurred while loading 2D area solutions: {e}", exc_info=True)
|
1079
|
+
return None
|
1080
|
+
|
1081
|
+
|
1082
|
+
@classmethod
|
1083
|
+
@hdf_operation
|
1084
|
+
def build_2d_area_face_hydraulic_information(cls, hdf_input: Union[str, Path, h5py.File], area_name: Optional[str] = None, ras_object=None) -> Optional[List[List[np.ndarray]]]:
|
1085
|
+
"""
|
1086
|
+
Build face hydraulic information tables (elevation, area, wetted perimeter, Manning's n) for each face in 2D Flow Areas.
|
1087
|
+
|
1088
|
+
Args:
|
1089
|
+
hdf_input (Union[str, Path, h5py.File]): The HDF5 file path or open HDF5 file object.
|
1090
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, builds for all areas.
|
1091
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1092
|
+
|
1093
|
+
Returns:
|
1094
|
+
Optional[List[List[np.ndarray]]]: Nested lists containing hydraulic information for each face in each 2D Flow Area.
|
1095
|
+
|
1096
|
+
Example:
|
1097
|
+
>>> hydraulic_info = RasHdf.build_2d_area_face_hydraulic_information("03")
|
1098
|
+
>>> print(hydraulic_info[0][0]) # First face of first area
|
1099
|
+
[[Elevation1, Area1, WettedPerim1, ManningN1],
|
1100
|
+
[Elevation2, Area2, WettedPerim2, ManningN2],
|
1101
|
+
...]
|
1102
|
+
"""
|
1103
|
+
try:
|
1104
|
+
ras_obj = ras_object if ras_object is not None else ras
|
1105
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_obj), 'r') as hdf_file:
|
1106
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1107
|
+
hydraulic_info_table = []
|
1108
|
+
|
1109
|
+
for area in two_d_area_names:
|
1110
|
+
face_elev_info = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Faces Area Elevation Info'])
|
1111
|
+
face_elev_values = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Faces Area Elevation Values'])
|
1112
|
+
|
1113
|
+
area_hydraulic_info = []
|
1114
|
+
for face in face_elev_info:
|
1115
|
+
start_row, count = face
|
1116
|
+
face_data = face_elev_values[start_row:start_row + count].copy()
|
1117
|
+
area_hydraulic_info.append(face_data)
|
1118
|
+
logging.info(f"Processed hydraulic information for face {face} in 2D Flow Area: {area}")
|
1119
|
+
|
1120
|
+
hydraulic_info_table.append(area_hydraulic_info)
|
1121
|
+
|
1122
|
+
return hydraulic_info_table
|
1123
|
+
|
1124
|
+
except KeyError as e:
|
1125
|
+
logging.error(f"Error building face hydraulic information: {e}")
|
1126
|
+
return None
|
1127
|
+
|
1128
|
+
@classmethod
|
1129
|
+
@hdf_operation
|
1130
|
+
def build_2d_area_face_point_coordinates_list(cls, hdf_input: Union[str, Path, h5py.File], area_name: Optional[str] = None, ras_object=None) -> Optional[List[np.ndarray]]:
|
1131
|
+
"""
|
1132
|
+
Build a list of face point coordinates for each 2D Flow Area.
|
1133
|
+
|
1134
|
+
Args:
|
1135
|
+
hdf_input (Union[str, Path, h5py.File]): The HDF5 file path or open HDF5 file object.
|
1136
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, builds for all areas.
|
1137
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1138
|
+
|
1139
|
+
Returns:
|
1140
|
+
Optional[List[np.ndarray]]: List containing arrays of face point coordinates for each 2D Flow Area.
|
1141
|
+
|
1142
|
+
Example:
|
1143
|
+
>>> face_coords_list = RasHdf.build_2d_area_face_point_coordinates_list("03")
|
1144
|
+
>>> print(face_coords_list[0]) # Coordinates for first area
|
1145
|
+
[[X1, Y1], [X2, Y2], ...]
|
1146
|
+
"""
|
1147
|
+
try:
|
1148
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
1149
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1150
|
+
face_point_coords_list = []
|
1151
|
+
|
1152
|
+
for area in two_d_area_names:
|
1153
|
+
face_points = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Face Points Coordinates'])
|
1154
|
+
face_point_coords_list.append(face_points)
|
1155
|
+
logging.info(f"Built face point coordinates list for 2D Flow Area: {area}")
|
1156
|
+
|
1157
|
+
return face_point_coords_list
|
1158
|
+
|
1159
|
+
except KeyError as e:
|
1160
|
+
logging.error(f"Error building face point coordinates list: {e}")
|
1161
|
+
return None
|
1162
|
+
|
1163
|
+
@classmethod
|
1164
|
+
@hdf_operation
|
1165
|
+
def build_2d_area_face_profile(cls, hdf_input: Union[str, Path, h5py.File], area_name: Optional[str] = None, ras_object=None, n_face_profile_points: int = 10) -> Optional[List[np.ndarray]]:
|
1166
|
+
"""
|
1167
|
+
Build face profiles representing sub-grid terrain for each face in 2D Flow Areas.
|
1168
|
+
|
1169
|
+
Args:
|
1170
|
+
hdf_input (Union[str, Path, h5py.File]): The HDF5 file path or open HDF5 file object.
|
1171
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, builds for all areas.
|
1172
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1173
|
+
n_face_profile_points (int): Number of points to interpolate along each face profile.
|
1174
|
+
|
1175
|
+
Returns:
|
1176
|
+
Optional[List[np.ndarray]]: List containing arrays of profile points for each face in each 2D Flow Area.
|
1177
|
+
|
1178
|
+
Example:
|
1179
|
+
>>> face_profiles = RasHdf.build_2d_area_face_profile("03", n_face_profile_points=20)
|
1180
|
+
>>> print(face_profiles[0][0]) # Profile points for first face of first area
|
1181
|
+
[[X1, Y1, Z1], [X2, Y2, Z2], ...]
|
1182
|
+
"""
|
1183
|
+
try:
|
1184
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
1185
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1186
|
+
print(f"Building face profiles for {len(two_d_area_names)} 2D Flow Areas")
|
1187
|
+
print(f"Area names: {two_d_area_names}")
|
1188
|
+
face_profiles = []
|
1189
|
+
|
1190
|
+
for area in two_d_area_names:
|
1191
|
+
face_faces = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Faces FacePoint Indexes'])
|
1192
|
+
face_point_coords = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Face Points Coordinates'])
|
1193
|
+
profile_points_all_faces = []
|
1194
|
+
|
1195
|
+
for face in face_faces:
|
1196
|
+
face_start, face_end = face
|
1197
|
+
start_coords = face_point_coords[face_start]
|
1198
|
+
end_coords = face_point_coords[face_end]
|
1199
|
+
|
1200
|
+
length = cls.horizontal_distance(start_coords, end_coords)
|
1201
|
+
stations = np.linspace(0, length, n_face_profile_points, endpoint=True)
|
1202
|
+
|
1203
|
+
interpolated_points = np.array([
|
1204
|
+
start_coords + (end_coords - start_coords) * i / (n_face_profile_points - 1)
|
1205
|
+
for i in range(n_face_profile_points)
|
1206
|
+
])
|
1207
|
+
|
1208
|
+
# Interpolate Z coordinates (assuming a method exists)
|
1209
|
+
interpolated_points = cls.interpolate_z_coords(interpolated_points)
|
1210
|
+
|
1211
|
+
profile_points_all_faces.append(interpolated_points)
|
1212
|
+
logging.info(f"Built face profile for face {face} in 2D Flow Area: {area}")
|
1213
|
+
|
1214
|
+
face_profiles.append(profile_points_all_faces)
|
1215
|
+
|
1216
|
+
return face_profiles
|
1217
|
+
|
1218
|
+
except KeyError as e:
|
1219
|
+
logging.error(f"Error building face profiles: {e}")
|
1220
|
+
return None
|
1221
|
+
|
1222
|
+
@classmethod
|
1223
|
+
@hdf_operation
|
1224
|
+
def build_face_facepoints(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[List[np.ndarray]]:
|
1225
|
+
"""
|
1226
|
+
Build face's facepoint list for each 2D Flow Area.
|
1227
|
+
|
1228
|
+
Args:
|
1229
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
1230
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, builds for all areas.
|
1231
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1232
|
+
|
1233
|
+
Returns:
|
1234
|
+
Optional[List[np.ndarray]]: List containing arrays of face point indexes for each face in each 2D Flow Area.
|
1235
|
+
|
1236
|
+
Example:
|
1237
|
+
>>> face_facepoints = RasHdf.build_face_facepoints("03")
|
1238
|
+
>>> print(face_facepoints[0][0]) # FacePoint indexes for first face of first area
|
1239
|
+
[start_idx, end_idx]
|
1240
|
+
"""
|
1241
|
+
try:
|
1242
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
1243
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1244
|
+
face_facepoints_list = []
|
1245
|
+
|
1246
|
+
for area in two_d_area_names:
|
1247
|
+
face_facepoints = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Faces FacePoint Indexes'])
|
1248
|
+
face_facepoints_list.append(face_facepoints)
|
1249
|
+
logging.info(f"Built face facepoints list for 2D Flow Area: {area}")
|
1250
|
+
|
1251
|
+
return face_facepoints_list
|
1252
|
+
|
1253
|
+
except KeyError as e:
|
1254
|
+
logging.error(f"Error building face facepoints list: {e}")
|
1255
|
+
return None
|
1256
|
+
|
1257
|
+
@classmethod
|
1258
|
+
@hdf_operation
|
1259
|
+
def build_2d_area_boundaries(cls, hdf_input: Union[str, Path], area_name: Optional[str] = None, ras_object=None) -> Optional[Tuple[int, np.ndarray, List[str], List[str], List[str], np.ndarray, np.ndarray]]:
|
1260
|
+
"""
|
1261
|
+
Build boundaries with their point lists for each 2D Flow Area.
|
1262
|
+
|
1263
|
+
Args:
|
1264
|
+
hdf_input (Union[str, Path]): The plan number or full path to the HDF file.
|
1265
|
+
area_name (Optional[str]): Name of the 2D Flow Area. If None, builds for all areas.
|
1266
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1267
|
+
|
1268
|
+
Returns:
|
1269
|
+
Optional[Tuple[int, np.ndarray, List[str], List[str], List[str], np.ndarray, np.ndarray]]:
|
1270
|
+
Tuple containing total boundaries, boundary IDs, boundary names, associated 2D Flow Area names, boundary types,
|
1271
|
+
total points per boundary, and boundary point lists.
|
1272
|
+
|
1273
|
+
Example:
|
1274
|
+
>>> total_boundaries, boundary_ids, boundary_names, flow_area_names, boundary_types, total_points, boundary_points = RasHdf.build_2d_area_boundaries("03")
|
1275
|
+
>>> print(total_boundaries)
|
1276
|
+
5
|
1277
|
+
"""
|
1278
|
+
try:
|
1279
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
1280
|
+
two_d_area_names = cls.get_2d_flow_area_names(hdf_file, ras_object=ras_object)
|
1281
|
+
total_boundaries = 0
|
1282
|
+
boundary_ids = []
|
1283
|
+
boundary_names = []
|
1284
|
+
flow_area_names = []
|
1285
|
+
boundary_types = []
|
1286
|
+
total_points_per_boundary = []
|
1287
|
+
boundary_points_list = []
|
1288
|
+
|
1289
|
+
for area in two_d_area_names:
|
1290
|
+
boundary_points = np.array(hdf_file[f'Geometry/2D Flow Areas/{area}/Boundary Points'])
|
1291
|
+
if boundary_points.size == 0:
|
1292
|
+
logging.warning(f"No boundary points found for 2D Flow Area: {area}")
|
1293
|
+
continue
|
1294
|
+
|
1295
|
+
current_boundary_id = boundary_points[0][0]
|
1296
|
+
current_boundary_points = [boundary_points[0][2], boundary_points[0][3]]
|
1297
|
+
boundary_id = current_boundary_id
|
1298
|
+
|
1299
|
+
for point in boundary_points[1:]:
|
1300
|
+
if point[0] == current_boundary_id:
|
1301
|
+
current_boundary_points.append(point[3])
|
1302
|
+
else:
|
1303
|
+
# Save the completed boundary
|
1304
|
+
boundary_ids.append(current_boundary_id)
|
1305
|
+
boundary_names.append(point[0]) # Assuming boundary name is stored here
|
1306
|
+
flow_area_names.append(area)
|
1307
|
+
boundary_types.append(point[2]) # Assuming boundary type is stored here
|
1308
|
+
total_points_per_boundary.append(len(current_boundary_points))
|
1309
|
+
boundary_points_list.append(np.array(current_boundary_points))
|
1310
|
+
total_boundaries += 1
|
1311
|
+
|
1312
|
+
# Start a new boundary
|
1313
|
+
current_boundary_id = point[0]
|
1314
|
+
current_boundary_points = [point[2], point[3]]
|
1315
|
+
|
1316
|
+
# Save the last boundary
|
1317
|
+
boundary_ids.append(current_boundary_id)
|
1318
|
+
boundary_names.append(boundary_points[-1][0]) # Assuming boundary name is stored here
|
1319
|
+
flow_area_names.append(area)
|
1320
|
+
boundary_types.append(boundary_points[-1][2]) # Assuming boundary type is stored here
|
1321
|
+
total_points_per_boundary.append(len(current_boundary_points))
|
1322
|
+
boundary_points_list.append(np.array(current_boundary_points))
|
1323
|
+
total_boundaries += 1
|
1324
|
+
|
1325
|
+
logging.info(f"Built boundaries for 2D Flow Area: {area}, Total Boundaries: {total_boundaries}")
|
1326
|
+
|
1327
|
+
return (total_boundaries, np.array(boundary_ids), boundary_names, flow_area_names, boundary_types, np.array(total_points_per_boundary), np.array(boundary_points_list))
|
1328
|
+
|
1329
|
+
except KeyError as e:
|
1330
|
+
logging.error(f"Error building boundaries: {e}")
|
1331
|
+
return None
|
1332
|
+
|
1333
|
+
# Helper Methods for New Functionalities
|
1334
|
+
|
1335
|
+
|
1336
|
+
@classmethod
|
1337
|
+
def horizontal_distance(cls, coord1: np.ndarray, coord2: np.ndarray) -> float:
|
1338
|
+
"""
|
1339
|
+
Calculate the horizontal distance between two coordinate points.
|
1340
|
+
|
1341
|
+
Args:
|
1342
|
+
coord1 (np.ndarray): First coordinate point [X, Y].
|
1343
|
+
coord2 (np.ndarray): Second coordinate point [X, Y].
|
1344
|
+
|
1345
|
+
Returns:
|
1346
|
+
float: Horizontal distance.
|
1347
|
+
|
1348
|
+
Example:
|
1349
|
+
>>> distance = RasHdf.horizontal_distance([0, 0], [3, 4])
|
1350
|
+
>>> print(distance)
|
1351
|
+
5.0
|
1352
|
+
"""
|
1353
|
+
return np.linalg.norm(coord2 - coord1)
|
1354
|
+
|
1355
|
+
@classmethod
|
1356
|
+
def interpolate_z_coords(cls, points: np.ndarray) -> np.ndarray:
|
1357
|
+
"""
|
1358
|
+
Interpolate Z coordinates for a set of points.
|
1359
|
+
|
1360
|
+
Args:
|
1361
|
+
points (np.ndarray): Array of points with [X, Y].
|
1362
|
+
|
1363
|
+
Returns:
|
1364
|
+
np.ndarray: Array of points with [X, Y, Z].
|
1365
|
+
|
1366
|
+
Example:
|
1367
|
+
>>> interpolated = RasHdf.interpolate_z_coords(np.array([[0,0], [1,1]]))
|
1368
|
+
>>> print(interpolated)
|
1369
|
+
[[0, 0, Z0],
|
1370
|
+
[1, 1, Z1]]
|
1371
|
+
"""
|
1372
|
+
# Placeholder for actual interpolation logic
|
1373
|
+
# This should be replaced with the appropriate interpolation method
|
1374
|
+
z_coords = np.zeros((points.shape[0], 1)) # Assuming Z=0 for simplicity
|
1375
|
+
return np.hstack((points, z_coords))
|
1376
|
+
|
1377
|
+
|
1378
|
+
|
1379
|
+
|
1380
|
+
|
1381
|
+
|
1382
|
+
|
1383
|
+
|
1384
|
+
@classmethod
|
1385
|
+
@hdf_operation
|
1386
|
+
def extract_string_from_hdf(
|
1387
|
+
cls,
|
1388
|
+
hdf_input: Union[str, Path],
|
1389
|
+
hdf_path: str,
|
1390
|
+
ras_object: Optional["RasPrj"] = None
|
1391
|
+
) -> str:
|
1392
|
+
"""
|
1393
|
+
Extract string from HDF object at a given path.
|
1394
|
+
|
1395
|
+
Args:
|
1396
|
+
hdf_input (Union[str, Path]): Either the plan number or the full path to the HDF file.
|
1397
|
+
hdf_path (str): Path of the object in the HDF file.
|
1398
|
+
ras_object (Optional["RasPrj"]): Specific RAS object to use. If None, uses the global ras instance.
|
1399
|
+
|
1400
|
+
Returns:
|
1401
|
+
str: Extracted string from the specified HDF object.
|
1402
|
+
|
1403
|
+
Raises:
|
1404
|
+
ValueError: If no HDF file is found for the given plan number.
|
1405
|
+
FileNotFoundError: If the specified HDF file does not exist.
|
1406
|
+
KeyError: If the specified hdf_path is not found in the file.
|
1407
|
+
|
1408
|
+
Example:
|
1409
|
+
>>> result = RasHdf.extract_string_from_hdf("path/to/file.hdf", "/Results/Summary/Compute Messages (text)")
|
1410
|
+
>>> print(result)
|
1411
|
+
"""
|
1412
|
+
with h5py.File(cls._get_hdf_filename(hdf_input, ras_object), 'r') as hdf_file:
|
1413
|
+
try:
|
1414
|
+
hdf_object = hdf_file[hdf_path]
|
1415
|
+
if isinstance(hdf_object, h5py.Group):
|
1416
|
+
return f"Group: {hdf_path}\nContents: {list(hdf_object.keys())}"
|
1417
|
+
elif isinstance(hdf_object, h5py.Dataset):
|
1418
|
+
data = hdf_object[()]
|
1419
|
+
if isinstance(data, bytes):
|
1420
|
+
return data.decode('utf-8')
|
1421
|
+
elif isinstance(data, np.ndarray) and data.dtype.kind == 'S':
|
1422
|
+
return [v.decode('utf-8') for v in data]
|
1423
|
+
else:
|
1424
|
+
return str(data)
|
1425
|
+
else:
|
1426
|
+
return f"Unsupported object type: {type(hdf_object)}"
|
1427
|
+
except KeyError:
|
1428
|
+
raise KeyError(f"Path not found: {hdf_path}")
|
1429
|
+
|
1430
|
+
@classmethod
|
1431
|
+
@staticmethod
|
1432
|
+
def decode_byte_strings(dataframe: pd.DataFrame) -> pd.DataFrame:
|
1433
|
+
"""
|
1434
|
+
Decodes byte strings in a DataFrame to regular string objects.
|
1435
|
+
|
1436
|
+
This function converts columns with byte-encoded strings (e.g., b'string') into UTF-8 decoded strings.
|
1437
|
+
|
1438
|
+
Args:
|
1439
|
+
dataframe (pd.DataFrame): The DataFrame containing byte-encoded string columns.
|
1440
|
+
|
1441
|
+
Returns:
|
1442
|
+
pd.DataFrame: The DataFrame with byte strings decoded to regular strings.
|
1443
|
+
|
1444
|
+
Example:
|
1445
|
+
>>> df = pd.DataFrame({'A': [b'hello', b'world'], 'B': [1, 2]})
|
1446
|
+
>>> decoded_df = RasHdf.decode_byte_strings(df)
|
1447
|
+
>>> print(decoded_df)
|
1448
|
+
A B
|
1449
|
+
0 hello 1
|
1450
|
+
1 world 2
|
1451
|
+
"""
|
1452
|
+
str_df = dataframe.select_dtypes(['object'])
|
1453
|
+
str_df = str_df.stack().str.decode('utf-8').unstack()
|
1454
|
+
for col in str_df:
|
1455
|
+
dataframe[col] = str_df[col]
|
1456
|
+
return dataframe
|
1457
|
+
|
1458
|
+
@classmethod
|
1459
|
+
@staticmethod
|
1460
|
+
def perform_kdtree_query(
|
1461
|
+
reference_points: np.ndarray,
|
1462
|
+
query_points: np.ndarray,
|
1463
|
+
max_distance: float = 2.0
|
1464
|
+
) -> np.ndarray:
|
1465
|
+
"""
|
1466
|
+
Performs a KDTree query between two datasets and returns indices with distances exceeding max_distance set to -1.
|
1467
|
+
|
1468
|
+
Args:
|
1469
|
+
reference_points (np.ndarray): The reference dataset for KDTree.
|
1470
|
+
query_points (np.ndarray): The query dataset to search against KDTree of reference_points.
|
1471
|
+
max_distance (float, optional): The maximum distance threshold. Indices with distances greater than this are set to -1. Defaults to 2.0.
|
1472
|
+
|
1473
|
+
Returns:
|
1474
|
+
np.ndarray: Array of indices from reference_points that are nearest to each point in query_points.
|
1475
|
+
Indices with distances > max_distance are set to -1.
|
1476
|
+
|
1477
|
+
Example:
|
1478
|
+
>>> ref_points = np.array([[0, 0], [1, 1], [2, 2]])
|
1479
|
+
>>> query_points = np.array([[0.5, 0.5], [3, 3]])
|
1480
|
+
>>> result = RasHdf.perform_kdtree_query(ref_points, query_points)
|
1481
|
+
>>> print(result)
|
1482
|
+
array([ 0, -1])
|
1483
|
+
"""
|
1484
|
+
dist, snap = KDTree(reference_points).query(query_points, distance_upper_bound=max_distance)
|
1485
|
+
snap[dist > max_distance] = -1
|
1486
|
+
return snap
|
1487
|
+
|
1488
|
+
@classmethod
|
1489
|
+
@staticmethod
|
1490
|
+
def find_nearest_neighbors(points: np.ndarray, max_distance: float = 2.0) -> np.ndarray:
|
1491
|
+
"""
|
1492
|
+
Creates a self KDTree for dataset points and finds nearest neighbors excluding self,
|
1493
|
+
with distances above max_distance set to -1.
|
1494
|
+
|
1495
|
+
Args:
|
1496
|
+
points (np.ndarray): The dataset to build the KDTree from and query against itself.
|
1497
|
+
max_distance (float, optional): The maximum distance threshold. Indices with distances
|
1498
|
+
greater than max_distance are set to -1. Defaults to 2.0.
|
1499
|
+
|
1500
|
+
Returns:
|
1501
|
+
np.ndarray: Array of indices representing the nearest neighbor in points for each point in points.
|
1502
|
+
Indices with distances > max_distance or self-matches are set to -1.
|
1503
|
+
|
1504
|
+
Example:
|
1505
|
+
>>> points = np.array([[0, 0], [1, 1], [2, 2], [10, 10]])
|
1506
|
+
>>> result = RasHdf.find_nearest_neighbors(points)
|
1507
|
+
>>> print(result)
|
1508
|
+
array([1, 0, 1, -1])
|
1509
|
+
"""
|
1510
|
+
dist, snap = KDTree(points).query(points, k=2, distance_upper_bound=max_distance)
|
1511
|
+
snap[dist > max_distance] = -1
|
1512
|
+
|
1513
|
+
snp = pd.DataFrame(snap, index=np.arange(len(snap)))
|
1514
|
+
snp = snp.replace(-1, np.nan)
|
1515
|
+
snp.loc[snp[0] == snp.index, 0] = np.nan
|
1516
|
+
snp.loc[snp[1] == snp.index, 1] = np.nan
|
1517
|
+
filled = snp[0].fillna(snp[1])
|
1518
|
+
snapped = filled.fillna(-1).astype(np.int64).to_numpy()
|
1519
|
+
return snapped
|
1520
|
+
|
1521
|
+
@classmethod
|
1522
|
+
@staticmethod
|
1523
|
+
def consolidate_dataframe(
|
1524
|
+
dataframe: pd.DataFrame,
|
1525
|
+
group_by: Optional[Union[str, List[str]]] = None,
|
1526
|
+
pivot_columns: Optional[Union[str, List[str]]] = None,
|
1527
|
+
level: Optional[int] = None,
|
1528
|
+
n_dimensional: bool = False,
|
1529
|
+
aggregation_method: Union[str, Callable] = 'list'
|
1530
|
+
) -> pd.DataFrame:
|
1531
|
+
"""
|
1532
|
+
Consolidate rows in a DataFrame by merging duplicate values into lists or using a specified aggregation function.
|
1533
|
+
|
1534
|
+
Args:
|
1535
|
+
dataframe (pd.DataFrame): The DataFrame to consolidate.
|
1536
|
+
group_by (Optional[Union[str, List[str]]]): Columns or indices to group by.
|
1537
|
+
pivot_columns (Optional[Union[str, List[str]]]): Columns to pivot.
|
1538
|
+
level (Optional[int]): Level of multi-index to group by.
|
1539
|
+
n_dimensional (bool): If True, use a pivot table for N-Dimensional consolidation.
|
1540
|
+
aggregation_method (Union[str, Callable]): Aggregation method, e.g., 'list' to aggregate into lists.
|
1541
|
+
|
1542
|
+
Returns:
|
1543
|
+
pd.DataFrame: The consolidated DataFrame.
|
1544
|
+
|
1545
|
+
Example:
|
1546
|
+
>>> df = pd.DataFrame({'A': [1, 1, 2], 'B': [4, 5, 6], 'C': [7, 8, 9]})
|
1547
|
+
>>> result = RasHdf.consolidate_dataframe(df, group_by='A')
|
1548
|
+
>>> print(result)
|
1549
|
+
B C
|
1550
|
+
A
|
1551
|
+
1 [4, 5] [7, 8]
|
1552
|
+
2 [6] [9]
|
1553
|
+
"""
|
1554
|
+
if aggregation_method == 'list':
|
1555
|
+
agg_func = lambda x: tuple(x)
|
1556
|
+
else:
|
1557
|
+
agg_func = aggregation_method
|
1558
|
+
|
1559
|
+
if n_dimensional:
|
1560
|
+
result = dataframe.pivot_table(group_by, pivot_columns, aggfunc=agg_func)
|
1561
|
+
else:
|
1562
|
+
result = dataframe.groupby(group_by, level=level).agg(agg_func).applymap(list)
|
1563
|
+
|
1564
|
+
return result
|
1565
|
+
|
1566
|
+
@classmethod
|
1567
|
+
@staticmethod
|
1568
|
+
def find_nearest_value(array: Union[list, np.ndarray], target_value: Union[int, float]) -> Union[int, float]:
|
1569
|
+
"""
|
1570
|
+
Finds the nearest value in a NumPy array to the specified target value.
|
1571
|
+
|
1572
|
+
Args:
|
1573
|
+
array (Union[list, np.ndarray]): The array to search within.
|
1574
|
+
target_value (Union[int, float]): The value to find the nearest neighbor to.
|
1575
|
+
|
1576
|
+
Returns:
|
1577
|
+
Union[int, float]: The nearest value in the array to the specified target value.
|
1578
|
+
|
1579
|
+
Example:
|
1580
|
+
>>> arr = np.array([1, 3, 5, 7, 9])
|
1581
|
+
>>> result = RasHdf.find_nearest_value(arr, 6)
|
1582
|
+
>>> print(result)
|
1583
|
+
5
|
1584
|
+
"""
|
1585
|
+
array = np.asarray(array)
|
1586
|
+
idx = (np.abs(array - target_value)).argmin()
|
1587
|
+
return array[idx]
|
1588
|
+
|
1589
|
+
@staticmethod
|
1590
|
+
def _get_hdf_filename(hdf_input: Union[str, Path, h5py.File], ras_object=None) -> Path:
|
1591
|
+
"""
|
1592
|
+
Get the HDF filename from the input.
|
1593
|
+
|
1594
|
+
Args:
|
1595
|
+
hdf_input (Union[str, Path, h5py.File]): The plan number, full path to the HDF file as a string, a Path object, or an h5py.File object.
|
1596
|
+
ras_object (RasPrj, optional): The RAS project object. If None, uses the global ras instance.
|
1597
|
+
|
1598
|
+
Returns:
|
1599
|
+
Path: The full path to the HDF file as a Path object.
|
1600
|
+
|
1601
|
+
Raises:
|
1602
|
+
ValueError: If no HDF file is found for the given plan number or if the input type is invalid.
|
1603
|
+
FileNotFoundError: If the specified HDF file does not exist.
|
1604
|
+
"""
|
1605
|
+
|
1606
|
+
# If hdf_input is already an h5py.File object, return its filename
|
1607
|
+
if isinstance(hdf_input, h5py.File):
|
1608
|
+
return Path(hdf_input.filename)
|
1609
|
+
|
1610
|
+
# Convert to Path object if it's a string
|
1611
|
+
hdf_input = Path(hdf_input)
|
1612
|
+
|
1613
|
+
# If hdf_input is a file path, return it directly
|
1614
|
+
if hdf_input.is_file():
|
1615
|
+
return hdf_input
|
1616
|
+
|
1617
|
+
# If hdf_input is not a file path, assume it's a plan number and require ras_object
|
1618
|
+
ras_obj = ras_object or ras
|
1619
|
+
if not ras_obj.initialized:
|
1620
|
+
raise ValueError("ras_object is not initialized. ras_object is required when hdf_input is not a direct file path.")
|
1621
|
+
|
1622
|
+
plan_info = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == str(hdf_input)]
|
1623
|
+
if plan_info.empty:
|
1624
|
+
raise ValueError(f"No HDF file found for plan number {hdf_input}")
|
1625
|
+
|
1626
|
+
hdf_filename = Path(plan_info.iloc[0]['HDF_Results_Path'])
|
1627
|
+
if not hdf_filename.is_file():
|
1628
|
+
raise FileNotFoundError(f"HDF file not found: {hdf_filename}")
|
1629
|
+
|
1630
|
+
return hdf_filename
|
1631
|
+
|
1632
|
+
|
1633
|
+
|
1634
|
+
|
1635
|
+
def save_dataframe_to_hdf(
|
1636
|
+
dataframe: pd.DataFrame,
|
1637
|
+
hdf_parent_group: h5py.Group,
|
1638
|
+
dataset_name: str,
|
1639
|
+
attributes: Optional[Dict[str, Union[int, float, str]]] = None,
|
1640
|
+
fill_value: Union[int, float, str] = -9999,
|
1641
|
+
**kwargs: Any
|
1642
|
+
) -> h5py.Dataset:
|
1643
|
+
"""
|
1644
|
+
Save a pandas DataFrame to an HDF5 dataset within a specified parent group.
|
1645
|
+
|
1646
|
+
This function addresses limitations of `pd.to_hdf()` by using h5py to create and save datasets.
|
1647
|
+
|
1648
|
+
Args:
|
1649
|
+
dataframe (pd.DataFrame): The DataFrame to save.
|
1650
|
+
hdf_parent_group (h5py.Group): The parent HDF5 group where the dataset will be created.
|
1651
|
+
dataset_name (str): The name of the new dataset to add in the HDF5 parent group.
|
1652
|
+
attributes (Optional[Dict[str, Union[int, float, str]]]): A dictionary of attributes to add to the dataset.
|
1653
|
+
fill_value (Union[int, float, str]): The value to use for filling missing data.
|
1654
|
+
**kwargs: Additional keyword arguments passed to `hdf_parent_group.create_dataset()`.
|
1655
|
+
|
1656
|
+
Returns:
|
1657
|
+
h5py.Dataset: The created HDF5 dataset within the parent group.
|
1658
|
+
|
1659
|
+
Raises:
|
1660
|
+
ValueError: If the DataFrame columns are not consistent.
|
1661
|
+
|
1662
|
+
Example:
|
1663
|
+
>>> df = pd.DataFrame({'A': [1, 2, 3], 'B': ['a', 'b', 'c']})
|
1664
|
+
>>> with h5py.File('data.h5', 'w') as f:
|
1665
|
+
... group = f.create_group('my_group')
|
1666
|
+
... dataset = save_dataframe_to_hdf(df, group, 'my_dataset')
|
1667
|
+
>>> print(dataset)
|
1668
|
+
"""
|
1669
|
+
df = dataframe.copy()
|
1670
|
+
|
1671
|
+
# Replace '/' in column names with '-' to avoid issues in HDF5
|
1672
|
+
if df.columns.dtype == 'O':
|
1673
|
+
df.columns = df.columns.str.replace('/', '-', regex=False)
|
1674
|
+
|
1675
|
+
# Fill missing values with the specified fill_value
|
1676
|
+
df = df.fillna(fill_value)
|
1677
|
+
|
1678
|
+
# Identify string columns and ensure consistency
|
1679
|
+
string_cols = df.select_dtypes(include=['object']).columns
|
1680
|
+
if not string_cols.equals(df.select_dtypes(include=['object']).columns):
|
1681
|
+
raise ValueError("Inconsistent string columns detected")
|
1682
|
+
|
1683
|
+
# Encode string columns to bytes
|
1684
|
+
df[string_cols] = df[string_cols].applymap(lambda x: x.encode('utf-8')).astype('bytes')
|
1685
|
+
|
1686
|
+
# Prepare data for HDF5 dataset creation
|
1687
|
+
arr = df.to_records(index=False) if not isinstance(df.columns, pd.RangeIndex) else df.values
|
1688
|
+
|
1689
|
+
# Remove existing dataset if it exists
|
1690
|
+
if dataset_name in hdf_parent_group:
|
1691
|
+
del hdf_parent_group[dataset_name]
|
1692
|
+
|
1693
|
+
# Create the dataset in the HDF5 file
|
1694
|
+
dataset = hdf_parent_group.create_dataset(dataset_name, data=arr, **kwargs)
|
1695
|
+
|
1696
|
+
# Update dataset attributes if provided
|
1697
|
+
if attributes:
|
1698
|
+
dataset.attrs.update(attributes)
|
1699
|
+
|
1700
|
+
logging.info(f"Successfully saved DataFrame to dataset: {dataset_name}")
|
1701
|
+
return dataset
|
1702
|
+
|