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/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.info(f"No structures found in: {hdf_path}")
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) -> Dict[str, Any]:
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
- Dict[str, Any]
259
- Dictionary of structure attributes from the Geometry/Structures group.
260
- Returns empty dict if no structures are found.
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
- return HdfUtils.convert_hdf5_attrs_to_dict(hdf_file["Geometry/Structures"].attrs)
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()
@@ -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, RasPlan._update_unsteady_in_file, new_unsteady_flow_number)
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
- # Initialize DataFrames
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.unsteady_df = self._get_prj_entries('Unsteady')
169
- self.geom_df = self.get_geom_entries() # Use get_geom_entries instead of _get_prj_entries
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
- # Add Geom_File to plan_df
172
- self.plan_df['Geom_File'] = self.plan_df.apply(lambda row: self._get_geom_file_for_plan(row['plan_number']), axis=1)
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
- Get the geometry file path for a given plan number.
224
+ Extract the Flow File value from a plan file.
178
225
 
179
- Args:
180
- plan_number (str): The plan number to find the geometry file for.
226
+ Parameters:
227
+ -----------
228
+ plan_file_path : str or Path
229
+ Path to the plan file.
181
230
 
182
231
  Returns:
183
- str: The full path to the geometry HDF file, or None if not found.
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
- for line in content.splitlines():
193
- if line.startswith("Geom File="):
194
- geom_file = line.strip().split('=')[1]
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 reading plan file for geometry: {e}")
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': file_name[1:],
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 attempt to use the version from the global 'ras' object
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.