ras-commander 0.61.0__py3-none-any.whl → 0.64.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/HdfBndry.py +91 -7
- ras_commander/HdfInfiltration.py +7 -1
- ras_commander/HdfPlan.py +44 -5
- ras_commander/HdfResultsMesh.py +24 -14
- ras_commander/HdfResultsPlan.py +380 -364
- ras_commander/HdfStruc.py +30 -8
- ras_commander/RasMapper.py +1 -82
- ras_commander/RasPlan.py +218 -5
- ras_commander/RasPrj.py +146 -41
- ras_commander/__init__.py +3 -7
- {ras_commander-0.61.0.dist-info → ras_commander-0.64.0.dist-info}/METADATA +48 -47
- {ras_commander-0.61.0.dist-info → ras_commander-0.64.0.dist-info}/RECORD +15 -15
- {ras_commander-0.61.0.dist-info → ras_commander-0.64.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.61.0.dist-info → ras_commander-0.64.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.61.0.dist-info → ras_commander-0.64.0.dist-info}/top_level.txt +0 -0
ras_commander/HdfStruc.py
CHANGED
@@ -81,9 +81,20 @@ class HdfStruc:
|
|
81
81
|
try:
|
82
82
|
with h5py.File(hdf_path, 'r') as hdf:
|
83
83
|
if "Geometry/Structures" not in hdf:
|
84
|
-
logger.
|
84
|
+
logger.error(f"No Structures Found in the HDF, Empty Geodataframe Returned: {hdf_path}")
|
85
85
|
return GeoDataFrame()
|
86
86
|
|
87
|
+
# Check if required datasets exist
|
88
|
+
required_datasets = [
|
89
|
+
"Geometry/Structures/Centerline Info",
|
90
|
+
"Geometry/Structures/Centerline Points"
|
91
|
+
]
|
92
|
+
|
93
|
+
for dataset in required_datasets:
|
94
|
+
if dataset not in hdf:
|
95
|
+
logger.error(f"No Structures Found in the HDF, Empty Geodataframe Returned: {hdf_path}")
|
96
|
+
return GeoDataFrame()
|
97
|
+
|
87
98
|
def get_dataset_df(path: str) -> pd.DataFrame:
|
88
99
|
"""
|
89
100
|
Converts an HDF5 dataset to a pandas DataFrame.
|
@@ -244,7 +255,7 @@ class HdfStruc:
|
|
244
255
|
@staticmethod
|
245
256
|
@log_call
|
246
257
|
@standardize_input(file_type='geom_hdf')
|
247
|
-
def get_geom_structures_attrs(hdf_path: Path) ->
|
258
|
+
def get_geom_structures_attrs(hdf_path: Path) -> pd.DataFrame:
|
248
259
|
"""
|
249
260
|
Extracts structure attributes from a HEC-RAS geometry HDF file.
|
250
261
|
|
@@ -255,9 +266,9 @@ class HdfStruc:
|
|
255
266
|
|
256
267
|
Returns
|
257
268
|
-------
|
258
|
-
|
259
|
-
|
260
|
-
Returns empty
|
269
|
+
pd.DataFrame
|
270
|
+
DataFrame containing structure attributes from the Geometry/Structures group.
|
271
|
+
Returns empty DataFrame if no structures are found.
|
261
272
|
|
262
273
|
Notes
|
263
274
|
-----
|
@@ -268,8 +279,19 @@ class HdfStruc:
|
|
268
279
|
with h5py.File(hdf_path, 'r') as hdf_file:
|
269
280
|
if "Geometry/Structures" not in hdf_file:
|
270
281
|
logger.info(f"No structures found in the geometry file: {hdf_path}")
|
271
|
-
return
|
272
|
-
|
282
|
+
return pd.DataFrame()
|
283
|
+
|
284
|
+
# Get attributes and decode byte strings
|
285
|
+
attrs_dict = {}
|
286
|
+
for key, value in dict(hdf_file["Geometry/Structures"].attrs).items():
|
287
|
+
if isinstance(value, bytes):
|
288
|
+
attrs_dict[key] = value.decode('utf-8')
|
289
|
+
else:
|
290
|
+
attrs_dict[key] = value
|
291
|
+
|
292
|
+
# Create DataFrame with a single row index
|
293
|
+
return pd.DataFrame(attrs_dict, index=[0])
|
294
|
+
|
273
295
|
except Exception as e:
|
274
296
|
logger.error(f"Error reading geometry structures attributes: {str(e)}")
|
275
|
-
return
|
297
|
+
return pd.DataFrame()
|
ras_commander/RasMapper.py
CHANGED
@@ -21,85 +21,4 @@ from .HdfInfiltration import HdfInfiltration
|
|
21
21
|
|
22
22
|
class RasMapper:
|
23
23
|
"""Class for handling RAS Mapper operations and data extraction"""
|
24
|
-
|
25
|
-
@staticmethod
|
26
|
-
@log_call
|
27
|
-
def get_raster_map(hdf_path: Path) -> dict:
|
28
|
-
"""Read the raster map from HDF file and create value mapping
|
29
|
-
|
30
|
-
Args:
|
31
|
-
hdf_path: Path to the HDF file
|
32
|
-
|
33
|
-
Returns:
|
34
|
-
Dictionary mapping raster values to mukeys
|
35
|
-
"""
|
36
|
-
with h5py.File(hdf_path, 'r') as hdf:
|
37
|
-
raster_map_data = hdf['Raster Map'][:]
|
38
|
-
return {int(item[0]): item[1].decode('utf-8') for item in raster_map_data}
|
39
|
-
|
40
|
-
@staticmethod
|
41
|
-
@log_call
|
42
|
-
def clip_raster_with_boundary(raster_path: Path, boundary_path: Path):
|
43
|
-
"""Clip a raster using a boundary polygon
|
44
|
-
|
45
|
-
Args:
|
46
|
-
raster_path: Path to the raster file
|
47
|
-
boundary_path: Path to the boundary shapefile
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
Tuple of (clipped_image, transform, nodata_value)
|
51
|
-
"""
|
52
|
-
watershed = gpd.read_file(boundary_path)
|
53
|
-
raster = rasterio.open(raster_path)
|
54
|
-
|
55
|
-
out_image, out_transform = mask(raster, watershed.geometry, crop=True)
|
56
|
-
nodata = raster.nodatavals[0]
|
57
|
-
|
58
|
-
return out_image[0], out_transform, nodata
|
59
|
-
|
60
|
-
@staticmethod
|
61
|
-
@log_call
|
62
|
-
def calculate_zonal_stats(boundary_path: Path, raster_data, transform, nodata):
|
63
|
-
"""Calculate zonal statistics for a boundary
|
64
|
-
|
65
|
-
Args:
|
66
|
-
boundary_path: Path to boundary shapefile
|
67
|
-
raster_data: Numpy array of raster values
|
68
|
-
transform: Raster transform
|
69
|
-
nodata: Nodata value
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
List of statistics by zone
|
73
|
-
"""
|
74
|
-
watershed = gpd.read_file(boundary_path)
|
75
|
-
return zonal_stats(watershed, raster_data,
|
76
|
-
affine=transform,
|
77
|
-
nodata=nodata,
|
78
|
-
categorical=True)
|
79
|
-
|
80
|
-
# Example usage:
|
81
|
-
"""
|
82
|
-
# Initialize paths
|
83
|
-
raster_path = Path('input_files/gSSURGO_InfiltrationDC.tif')
|
84
|
-
boundary_path = Path('input_files/WF_Boundary_Simple.shp')
|
85
|
-
hdf_path = raster_path.with_suffix('.hdf')
|
86
|
-
|
87
|
-
# Get raster mapping
|
88
|
-
raster_map = RasMapper.get_raster_map(hdf_path)
|
89
|
-
|
90
|
-
# Clip raster with boundary
|
91
|
-
clipped_data, transform, nodata = RasMapper.clip_raster_with_boundary(
|
92
|
-
raster_path, boundary_path)
|
93
|
-
|
94
|
-
# Calculate zonal statistics
|
95
|
-
stats = RasMapper.calculate_zonal_stats(
|
96
|
-
boundary_path, clipped_data, transform, nodata)
|
97
|
-
|
98
|
-
# Calculate soil statistics
|
99
|
-
soil_stats = HdfInfiltration.calculate_soil_statistics(
|
100
|
-
stats, raster_map)
|
101
|
-
|
102
|
-
# Get significant mukeys (>1%)
|
103
|
-
significant_mukeys = HdfInfiltration.get_significant_mukeys(
|
104
|
-
soil_stats, threshold=1.0)
|
105
|
-
"""
|
24
|
+
# PLACEHOLDER FOR FUTURE DEVELOPMENT
|
ras_commander/RasPlan.py
CHANGED
@@ -50,6 +50,10 @@ List of Functions in RasPlan:
|
|
50
50
|
- update_plan_description(): Update the description in a plan file
|
51
51
|
- read_plan_description(): Read the description from a plan file
|
52
52
|
- update_simulation_date(): Update simulation start and end dates
|
53
|
+
- get_shortid(): Get the Short Identifier from a plan file
|
54
|
+
- set_shortid(): Set the Short Identifier in a plan file
|
55
|
+
- get_plan_title(): Get the Plan Title from a plan file
|
56
|
+
- set_plan_title(): Set the Plan Title in a plan file
|
53
57
|
|
54
58
|
|
55
59
|
"""
|
@@ -234,8 +238,17 @@ class RasPlan:
|
|
234
238
|
if not plan_file_path:
|
235
239
|
raise FileNotFoundError(f"Plan file not found: {plan_number}")
|
236
240
|
|
241
|
+
def update_unsteady_in_file(lines):
|
242
|
+
updated_lines = []
|
243
|
+
for line in lines:
|
244
|
+
if line.startswith("Flow File="):
|
245
|
+
updated_lines.append(f"Flow File=u{new_unsteady_flow_number}\n")
|
246
|
+
else:
|
247
|
+
updated_lines.append(line)
|
248
|
+
return updated_lines
|
249
|
+
|
237
250
|
try:
|
238
|
-
RasUtils.update_file(plan_file_path,
|
251
|
+
RasUtils.update_file(plan_file_path, update_unsteady_in_file)
|
239
252
|
except Exception as e:
|
240
253
|
raise Exception(f"Failed to update unsteady flow file: {e}")
|
241
254
|
|
@@ -245,10 +258,6 @@ class RasPlan:
|
|
245
258
|
ras_obj.flow_df = ras_obj.get_flow_entries()
|
246
259
|
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
247
260
|
|
248
|
-
@staticmethod
|
249
|
-
def _update_unsteady_in_file(lines, new_unsteady_flow_number):
|
250
|
-
return [f"Unsteady File=u{new_unsteady_flow_number}\n" if line.startswith("Unsteady File=u") else line for line in lines]
|
251
|
-
|
252
261
|
@staticmethod
|
253
262
|
@log_call
|
254
263
|
def set_num_cores(plan_number, num_cores, ras_object=None):
|
@@ -1257,3 +1266,207 @@ class RasPlan:
|
|
1257
1266
|
ras_object.plan_df = ras_object.get_plan_entries()
|
1258
1267
|
ras_object.unsteady_df = ras_object.get_unsteady_entries()
|
1259
1268
|
|
1269
|
+
@staticmethod
|
1270
|
+
@log_call
|
1271
|
+
def get_shortid(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
|
1272
|
+
"""
|
1273
|
+
Get the Short Identifier from a HEC-RAS plan file.
|
1274
|
+
|
1275
|
+
Args:
|
1276
|
+
plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
|
1277
|
+
ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
|
1278
|
+
|
1279
|
+
Returns:
|
1280
|
+
str: The Short Identifier from the plan file.
|
1281
|
+
|
1282
|
+
Raises:
|
1283
|
+
ValueError: If the plan file is not found.
|
1284
|
+
IOError: If there's an error reading from the plan file.
|
1285
|
+
|
1286
|
+
Example:
|
1287
|
+
>>> shortid = RasPlan.get_shortid('01')
|
1288
|
+
>>> print(f"Plan's Short Identifier: {shortid}")
|
1289
|
+
"""
|
1290
|
+
logger = get_logger(__name__)
|
1291
|
+
ras_obj = ras_object or ras
|
1292
|
+
ras_obj.check_initialized()
|
1293
|
+
|
1294
|
+
# Get the Short Identifier using get_plan_value
|
1295
|
+
shortid = RasPlan.get_plan_value(plan_number_or_path, "Short Identifier", ras_obj)
|
1296
|
+
|
1297
|
+
if shortid is None:
|
1298
|
+
logger.warning(f"Short Identifier not found in plan: {plan_number_or_path}")
|
1299
|
+
return ""
|
1300
|
+
|
1301
|
+
logger.info(f"Retrieved Short Identifier: {shortid}")
|
1302
|
+
return shortid
|
1303
|
+
|
1304
|
+
@staticmethod
|
1305
|
+
@log_call
|
1306
|
+
def set_shortid(plan_number_or_path: Union[str, Path], new_shortid: str, ras_object=None) -> None:
|
1307
|
+
"""
|
1308
|
+
Set the Short Identifier in a HEC-RAS plan file.
|
1309
|
+
|
1310
|
+
Args:
|
1311
|
+
plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
|
1312
|
+
new_shortid (str): The new Short Identifier to set (max 24 characters).
|
1313
|
+
ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
|
1314
|
+
|
1315
|
+
Raises:
|
1316
|
+
ValueError: If the plan file is not found or if new_shortid is too long.
|
1317
|
+
IOError: If there's an error updating the plan file.
|
1318
|
+
|
1319
|
+
Example:
|
1320
|
+
>>> RasPlan.set_shortid('01', 'NewShortIdentifier')
|
1321
|
+
"""
|
1322
|
+
logger = get_logger(__name__)
|
1323
|
+
ras_obj = ras_object or ras
|
1324
|
+
ras_obj.check_initialized()
|
1325
|
+
|
1326
|
+
# Ensure new_shortid is not too long (HEC-RAS limits short identifiers to 24 characters)
|
1327
|
+
if len(new_shortid) > 24:
|
1328
|
+
logger.warning(f"Short Identifier too long (24 char max). Truncating: {new_shortid}")
|
1329
|
+
new_shortid = new_shortid[:24]
|
1330
|
+
|
1331
|
+
# Get the plan file path
|
1332
|
+
plan_file_path = Path(plan_number_or_path)
|
1333
|
+
if not plan_file_path.is_file():
|
1334
|
+
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
|
1335
|
+
if not plan_file_path.exists():
|
1336
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
1337
|
+
raise ValueError(f"Plan file not found: {plan_file_path}")
|
1338
|
+
|
1339
|
+
try:
|
1340
|
+
# Read the file
|
1341
|
+
with open(plan_file_path, 'r') as file:
|
1342
|
+
lines = file.readlines()
|
1343
|
+
|
1344
|
+
# Update the Short Identifier line
|
1345
|
+
updated = False
|
1346
|
+
for i, line in enumerate(lines):
|
1347
|
+
if line.startswith("Short Identifier="):
|
1348
|
+
lines[i] = f"Short Identifier={new_shortid}\n"
|
1349
|
+
updated = True
|
1350
|
+
break
|
1351
|
+
|
1352
|
+
# If Short Identifier line not found, add it after Plan Title
|
1353
|
+
if not updated:
|
1354
|
+
for i, line in enumerate(lines):
|
1355
|
+
if line.startswith("Plan Title="):
|
1356
|
+
lines.insert(i+1, f"Short Identifier={new_shortid}\n")
|
1357
|
+
updated = True
|
1358
|
+
break
|
1359
|
+
|
1360
|
+
# If Plan Title not found either, add at the beginning
|
1361
|
+
if not updated:
|
1362
|
+
lines.insert(0, f"Short Identifier={new_shortid}\n")
|
1363
|
+
|
1364
|
+
# Write the updated content back to the file
|
1365
|
+
with open(plan_file_path, 'w') as file:
|
1366
|
+
file.writelines(lines)
|
1367
|
+
|
1368
|
+
logger.info(f"Updated Short Identifier in plan file to: {new_shortid}")
|
1369
|
+
|
1370
|
+
except IOError as e:
|
1371
|
+
logger.error(f"Error updating Short Identifier in plan file {plan_file_path}: {e}")
|
1372
|
+
raise ValueError(f"Error updating Short Identifier: {e}")
|
1373
|
+
|
1374
|
+
# Refresh RasPrj dataframes if ras_object provided
|
1375
|
+
if ras_object:
|
1376
|
+
ras_object.plan_df = ras_object.get_plan_entries()
|
1377
|
+
|
1378
|
+
@staticmethod
|
1379
|
+
@log_call
|
1380
|
+
def get_plan_title(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
|
1381
|
+
"""
|
1382
|
+
Get the Plan Title from a HEC-RAS plan file.
|
1383
|
+
|
1384
|
+
Args:
|
1385
|
+
plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
|
1386
|
+
ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
|
1387
|
+
|
1388
|
+
Returns:
|
1389
|
+
str: The Plan Title from the plan file.
|
1390
|
+
|
1391
|
+
Raises:
|
1392
|
+
ValueError: If the plan file is not found.
|
1393
|
+
IOError: If there's an error reading from the plan file.
|
1394
|
+
|
1395
|
+
Example:
|
1396
|
+
>>> title = RasPlan.get_plan_title('01')
|
1397
|
+
>>> print(f"Plan Title: {title}")
|
1398
|
+
"""
|
1399
|
+
logger = get_logger(__name__)
|
1400
|
+
ras_obj = ras_object or ras
|
1401
|
+
ras_obj.check_initialized()
|
1402
|
+
|
1403
|
+
# Get the Plan Title using get_plan_value
|
1404
|
+
title = RasPlan.get_plan_value(plan_number_or_path, "Plan Title", ras_obj)
|
1405
|
+
|
1406
|
+
if title is None:
|
1407
|
+
logger.warning(f"Plan Title not found in plan: {plan_number_or_path}")
|
1408
|
+
return ""
|
1409
|
+
|
1410
|
+
logger.info(f"Retrieved Plan Title: {title}")
|
1411
|
+
return title
|
1412
|
+
|
1413
|
+
@staticmethod
|
1414
|
+
@log_call
|
1415
|
+
def set_plan_title(plan_number_or_path: Union[str, Path], new_title: str, ras_object=None) -> None:
|
1416
|
+
"""
|
1417
|
+
Set the Plan Title in a HEC-RAS plan file.
|
1418
|
+
|
1419
|
+
Args:
|
1420
|
+
plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
|
1421
|
+
new_title (str): The new Plan Title to set.
|
1422
|
+
ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
|
1423
|
+
|
1424
|
+
Raises:
|
1425
|
+
ValueError: If the plan file is not found.
|
1426
|
+
IOError: If there's an error updating the plan file.
|
1427
|
+
|
1428
|
+
Example:
|
1429
|
+
>>> RasPlan.set_plan_title('01', 'Updated Plan Scenario')
|
1430
|
+
"""
|
1431
|
+
logger = get_logger(__name__)
|
1432
|
+
ras_obj = ras_object or ras
|
1433
|
+
ras_obj.check_initialized()
|
1434
|
+
|
1435
|
+
# Get the plan file path
|
1436
|
+
plan_file_path = Path(plan_number_or_path)
|
1437
|
+
if not plan_file_path.is_file():
|
1438
|
+
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
|
1439
|
+
if not plan_file_path.exists():
|
1440
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
1441
|
+
raise ValueError(f"Plan file not found: {plan_file_path}")
|
1442
|
+
|
1443
|
+
try:
|
1444
|
+
# Read the file
|
1445
|
+
with open(plan_file_path, 'r') as file:
|
1446
|
+
lines = file.readlines()
|
1447
|
+
|
1448
|
+
# Update the Plan Title line
|
1449
|
+
updated = False
|
1450
|
+
for i, line in enumerate(lines):
|
1451
|
+
if line.startswith("Plan Title="):
|
1452
|
+
lines[i] = f"Plan Title={new_title}\n"
|
1453
|
+
updated = True
|
1454
|
+
break
|
1455
|
+
|
1456
|
+
# If Plan Title line not found, add it at the beginning
|
1457
|
+
if not updated:
|
1458
|
+
lines.insert(0, f"Plan Title={new_title}\n")
|
1459
|
+
|
1460
|
+
# Write the updated content back to the file
|
1461
|
+
with open(plan_file_path, 'w') as file:
|
1462
|
+
file.writelines(lines)
|
1463
|
+
|
1464
|
+
logger.info(f"Updated Plan Title in plan file to: {new_title}")
|
1465
|
+
|
1466
|
+
except IOError as e:
|
1467
|
+
logger.error(f"Error updating Plan Title in plan file {plan_file_path}: {e}")
|
1468
|
+
raise ValueError(f"Error updating Plan Title: {e}")
|
1469
|
+
|
1470
|
+
# Refresh RasPrj dataframes if ras_object provided
|
1471
|
+
if ras_object:
|
1472
|
+
ras_object.plan_df = ras_object.get_plan_entries()
|
ras_commander/RasPrj.py
CHANGED
@@ -161,47 +161,92 @@ class RasPrj:
|
|
161
161
|
|
162
162
|
This method initializes DataFrames for plan, flow, unsteady, and geometry entries
|
163
163
|
by calling the _get_prj_entries method for each entry type.
|
164
|
+
Also extracts unsteady_number and geometry_number from plan files and adds them to plan_df.
|
164
165
|
"""
|
165
|
-
#
|
166
|
+
# Load unsteady first to ensure consistent handling of unsteady numbers
|
167
|
+
self.unsteady_df = self._get_prj_entries('Unsteady')
|
166
168
|
self.plan_df = self._get_prj_entries('Plan')
|
167
169
|
self.flow_df = self._get_prj_entries('Flow')
|
168
|
-
self.
|
169
|
-
|
170
|
+
self.geom_df = self.get_geom_entries()
|
171
|
+
|
172
|
+
# Initialize Geom_File column
|
173
|
+
self.plan_df['Geom_File'] = None
|
174
|
+
|
175
|
+
# Set geometry HDF paths
|
176
|
+
for idx, row in self.plan_df.iterrows():
|
177
|
+
try:
|
178
|
+
plan_file_path = row['full_path']
|
179
|
+
geom_number, geom_hdf_path = self._get_geom_file_from_plan(plan_file_path)
|
180
|
+
if geom_number:
|
181
|
+
self.plan_df.at[idx, 'Geom_File'] = geom_hdf_path
|
182
|
+
if not self.suppress_logging:
|
183
|
+
logger.info(f"Plan {row['plan_number']} uses geometry file {geom_number}")
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"Error processing plan file {row['plan_number']}: {e}")
|
186
|
+
|
187
|
+
def _get_geom_file_from_plan(self, plan_file_path):
|
188
|
+
"""
|
189
|
+
Extract the geometry number from a plan file by finding the Geom File value
|
190
|
+
and stripping the leading 'g'.
|
191
|
+
|
192
|
+
Parameters:
|
193
|
+
-----------
|
194
|
+
plan_file_path : str or Path
|
195
|
+
Path to the plan file.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
--------
|
199
|
+
tuple: (str, str)
|
200
|
+
A tuple containing (geometry_number, full_hdf_path) or (None, None) if not found.
|
201
|
+
geometry_number is the number after 'g' in the Geom File value
|
202
|
+
full_hdf_path is the path to the geometry HDF file
|
203
|
+
"""
|
204
|
+
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
205
|
+
|
206
|
+
if content is None:
|
207
|
+
return None, None
|
170
208
|
|
171
|
-
|
172
|
-
|
209
|
+
try:
|
210
|
+
match = re.search(r'Geom File=g(\d+)', content)
|
211
|
+
if match:
|
212
|
+
geom_number = match.group(1) # This gets just the number after 'g'
|
213
|
+
geom_file = f"g{geom_number}"
|
214
|
+
geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
|
215
|
+
if geom_hdf_path.exists():
|
216
|
+
return geom_number, str(geom_hdf_path)
|
217
|
+
except Exception as e:
|
218
|
+
logger.error(f"Error extracting geometry number from {plan_file_path}: {e}")
|
219
|
+
|
220
|
+
return None, None
|
173
221
|
|
174
|
-
|
175
|
-
def _get_geom_file_for_plan(self, plan_number):
|
222
|
+
def _get_flow_file_from_plan(self, plan_file_path):
|
176
223
|
"""
|
177
|
-
|
224
|
+
Extract the Flow File value from a plan file.
|
178
225
|
|
179
|
-
|
180
|
-
|
226
|
+
Parameters:
|
227
|
+
-----------
|
228
|
+
plan_file_path : str or Path
|
229
|
+
Path to the plan file.
|
181
230
|
|
182
231
|
Returns:
|
183
|
-
|
232
|
+
--------
|
233
|
+
str or None
|
234
|
+
The Flow File value or None if not found.
|
184
235
|
"""
|
185
|
-
plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
|
186
236
|
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
187
237
|
|
188
238
|
if content is None:
|
189
239
|
return None
|
190
240
|
|
191
241
|
try:
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
|
196
|
-
if geom_hdf_path.exists():
|
197
|
-
return str(geom_hdf_path)
|
198
|
-
else:
|
199
|
-
return None
|
242
|
+
match = re.search(r'Flow File=([^\s]+)', content)
|
243
|
+
if match:
|
244
|
+
return match.group(1)
|
200
245
|
except Exception as e:
|
201
|
-
logger.error(f"Error
|
246
|
+
logger.error(f"Error extracting Flow File from {plan_file_path}: {e}")
|
247
|
+
|
202
248
|
return None
|
203
249
|
|
204
|
-
|
205
250
|
@staticmethod
|
206
251
|
@log_call
|
207
252
|
def get_plan_value(
|
@@ -376,19 +421,16 @@ class RasPrj:
|
|
376
421
|
|
377
422
|
return plan_info
|
378
423
|
|
424
|
+
@log_call
|
379
425
|
def _get_prj_entries(self, entry_type):
|
380
426
|
"""
|
381
427
|
Extract entries of a specific type from the HEC-RAS project file.
|
382
|
-
|
428
|
+
|
383
429
|
Args:
|
384
430
|
entry_type (str): The type of entry to extract (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
385
|
-
|
431
|
+
|
386
432
|
Returns:
|
387
433
|
pd.DataFrame: A DataFrame containing the extracted entries.
|
388
|
-
|
389
|
-
Note:
|
390
|
-
This method reads the project file and extracts entries matching the specified type.
|
391
|
-
For 'Unsteady' entries, it parses additional information from the unsteady file.
|
392
434
|
"""
|
393
435
|
entries = []
|
394
436
|
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
@@ -400,28 +442,69 @@ class RasPrj:
|
|
400
442
|
if match:
|
401
443
|
file_name = match.group(1)
|
402
444
|
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
445
|
+
entry_number = file_name[1:] # Extract number portion without prefix
|
446
|
+
|
403
447
|
entry = {
|
404
|
-
f'{entry_type.lower()}_number':
|
448
|
+
f'{entry_type.lower()}_number': entry_number,
|
405
449
|
'full_path': full_path
|
406
450
|
}
|
407
|
-
|
408
|
-
if entry_type == 'Plan':
|
409
|
-
plan_info = self._parse_plan_file(Path(full_path))
|
410
|
-
entry.update(plan_info)
|
411
|
-
|
412
|
-
hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
|
413
|
-
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
414
|
-
|
451
|
+
|
415
452
|
if entry_type == 'Unsteady':
|
453
|
+
entry['unsteady_number'] = entry_number
|
416
454
|
unsteady_info = self._parse_unsteady_file(Path(full_path))
|
417
455
|
entry.update(unsteady_info)
|
456
|
+
else:
|
457
|
+
entry.update({
|
458
|
+
'unsteady_number': None,
|
459
|
+
'geometry_number': None,
|
460
|
+
'Short Identifier': None,
|
461
|
+
'Simulation Date': None
|
462
|
+
})
|
463
|
+
|
464
|
+
if entry_type == 'Plan':
|
465
|
+
plan_info = self._parse_plan_file(Path(full_path))
|
466
|
+
if plan_info:
|
467
|
+
# Handle Flow File (unsteady) number
|
468
|
+
flow_file = plan_info.get('Flow File')
|
469
|
+
if flow_file and flow_file.startswith('u'):
|
470
|
+
entry['unsteady_number'] = flow_file[1:]
|
471
|
+
else:
|
472
|
+
entry['unsteady_number'] = flow_file
|
473
|
+
|
474
|
+
# Handle Geom File number
|
475
|
+
geom_file = plan_info.get('Geom File')
|
476
|
+
if geom_file and geom_file.startswith('g'):
|
477
|
+
entry['geometry_number'] = geom_file[1:]
|
478
|
+
else:
|
479
|
+
entry['geometry_number'] = geom_file
|
480
|
+
|
481
|
+
entry['Short Identifier'] = plan_info.get('Short Identifier')
|
482
|
+
entry['Simulation Date'] = plan_info.get('Simulation Date')
|
483
|
+
|
484
|
+
# Update remaining fields
|
485
|
+
for key, value in plan_info.items():
|
486
|
+
if key not in ['unsteady_number', 'geometry_number', 'Short Identifier',
|
487
|
+
'Simulation Date', 'Flow File', 'Geom File']:
|
488
|
+
entry[key] = value
|
489
|
+
|
490
|
+
# Add HDF results path
|
491
|
+
hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
|
492
|
+
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
418
493
|
|
419
494
|
entries.append(entry)
|
495
|
+
|
496
|
+
df = pd.DataFrame(entries)
|
497
|
+
|
498
|
+
if not df.empty and entry_type == 'Plan':
|
499
|
+
first_cols = [f'{entry_type.lower()}_number', 'unsteady_number', 'geometry_number',
|
500
|
+
'Short Identifier', 'Simulation Date']
|
501
|
+
other_cols = [col for col in df.columns if col not in first_cols]
|
502
|
+
df = df[first_cols + other_cols]
|
503
|
+
|
504
|
+
return df
|
420
505
|
except Exception as e:
|
421
506
|
raise
|
422
507
|
|
423
|
-
return pd.DataFrame(entries)
|
424
|
-
|
425
508
|
def _parse_unsteady_file(self, unsteady_file_path):
|
426
509
|
"""
|
427
510
|
Parse an unsteady flow file and extract critical information.
|
@@ -811,6 +894,28 @@ class RasPrj:
|
|
811
894
|
|
812
895
|
return bc_info, unparsed_lines
|
813
896
|
|
897
|
+
@log_call
|
898
|
+
def get_unsteady_numbers_from_plans(self):
|
899
|
+
"""
|
900
|
+
Get all plans that use unsteady flow files.
|
901
|
+
|
902
|
+
Returns:
|
903
|
+
--------
|
904
|
+
pd.DataFrame
|
905
|
+
A DataFrame containing only plan entries that use unsteady flow files.
|
906
|
+
|
907
|
+
Raises:
|
908
|
+
-------
|
909
|
+
RuntimeError: If the project has not been initialized.
|
910
|
+
"""
|
911
|
+
self.check_initialized()
|
912
|
+
|
913
|
+
# Filter plan_df to only include plans with unsteady_number not None
|
914
|
+
unsteady_plans = self.plan_df[self.plan_df['unsteady_number'].notna()].copy()
|
915
|
+
|
916
|
+
logger.info(f"Found {len(unsteady_plans)} plans using unsteady flow files")
|
917
|
+
|
918
|
+
return unsteady_plans
|
814
919
|
|
815
920
|
# Create a global instance named 'ras'
|
816
921
|
# Defining the global instance allows the init_ras_project function to initialize the project.
|
@@ -881,8 +986,8 @@ def get_ras_exe(ras_version=None):
|
|
881
986
|
|
882
987
|
Args:
|
883
988
|
ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
|
884
|
-
If None, the function will
|
885
|
-
or a default path.
|
989
|
+
If None, the function will first check the global 'ras' object for a path.
|
990
|
+
If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
|
886
991
|
|
887
992
|
Returns:
|
888
993
|
str: The full path to the HEC-RAS executable.
|