ras-commander 0.61.0__py3-none-any.whl → 0.65.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
  """
@@ -101,11 +105,10 @@ class RasPlan:
101
105
  ras_obj = ras_object or ras
102
106
  ras_obj.check_initialized()
103
107
 
104
- # Ensure plan_number and new_geom are strings
105
108
  plan_number = str(plan_number).zfill(2)
106
109
  new_geom = str(new_geom).zfill(2)
107
110
 
108
- # Before doing anything, make sure the plan, geom, flow, and unsteady dataframes are current
111
+ # Update all dataframes
109
112
  ras_obj.plan_df = ras_obj.get_plan_entries()
110
113
  ras_obj.geom_df = ras_obj.get_geom_entries()
111
114
  ras_obj.flow_df = ras_obj.get_flow_entries()
@@ -115,18 +118,15 @@ class RasPlan:
115
118
  logger.error(f"Geometry {new_geom} not found in project.")
116
119
  raise ValueError(f"Geometry {new_geom} not found in project.")
117
120
 
118
- # Update the geometry for the specified plan
119
- ras_obj.plan_df.loc[ras_obj.plan_df['plan_number'] == plan_number, 'geom_number'] = new_geom
121
+ # Update all geometry-related columns
122
+ mask = ras_obj.plan_df['plan_number'] == plan_number
123
+ ras_obj.plan_df.loc[mask, 'geometry_number'] = new_geom
124
+ ras_obj.plan_df.loc[mask, 'Geom File'] = new_geom
125
+ geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom}"
126
+ ras_obj.plan_df.loc[mask, 'Geom Path'] = str(geom_path)
120
127
 
121
- logger.info(f"Geometry for plan {plan_number} set to {new_geom}")
122
- logger.debug("Updated plan DataFrame:")
123
- logger.debug(ras_obj.plan_df)
124
-
125
- # Update the project file
126
- prj_file_path = ras_obj.prj_file
127
- RasUtils.update_file(prj_file_path, RasPlan._update_geom_in_file, plan_number, new_geom)
128
-
129
- # Re-initialize the ras object to reflect changes
128
+ # Update project file and reinitialize
129
+ RasUtils.update_file(ras_obj.prj_file, RasPlan._update_geom_in_file, plan_number, new_geom)
130
130
  ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
131
131
 
132
132
  return ras_obj.plan_df
@@ -173,21 +173,26 @@ class RasPlan:
173
173
  ras_obj = ras_object or ras
174
174
  ras_obj.check_initialized()
175
175
 
176
- # Update the flow dataframe in the ras instance to ensure it is current
177
176
  ras_obj.flow_df = ras_obj.get_flow_entries()
178
177
 
179
178
  if new_steady_flow_number not in ras_obj.flow_df['flow_number'].values:
180
179
  raise ValueError(f"Steady flow number {new_steady_flow_number} not found in project file.")
181
180
 
182
- # Resolve the full path of the plan file
183
181
  plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
184
182
  if not plan_file_path:
185
183
  raise FileNotFoundError(f"Plan file not found: {plan_number}")
186
184
 
187
185
  RasUtils.update_file(plan_file_path, RasPlan._update_steady_in_file, new_steady_flow_number)
188
186
 
189
- # Update the ras object's dataframes
187
+ # Update dataframes and additional columns
190
188
  ras_obj.plan_df = ras_obj.get_plan_entries()
189
+ mask = ras_obj.plan_df['plan_number'] == plan_number
190
+ flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_steady_flow_number}"
191
+ ras_obj.plan_df.loc[mask, 'Flow File'] = new_steady_flow_number
192
+ ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
193
+ ras_obj.plan_df.loc[mask, 'unsteady_number'] = None
194
+
195
+ # Update remaining dataframes
191
196
  ras_obj.geom_df = ras_obj.get_geom_entries()
192
197
  ras_obj.flow_df = ras_obj.get_flow_entries()
193
198
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
@@ -223,32 +228,37 @@ class RasPlan:
223
228
  ras_obj = ras_object or ras
224
229
  ras_obj.check_initialized()
225
230
 
226
- # Update the unsteady dataframe in the ras instance to ensure it is current
227
231
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
228
232
 
229
233
  if new_unsteady_flow_number not in ras_obj.unsteady_df['unsteady_number'].values:
230
234
  raise ValueError(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
231
235
 
232
- # Get the full path of the plan file
233
236
  plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
234
237
  if not plan_file_path:
235
238
  raise FileNotFoundError(f"Plan file not found: {plan_number}")
236
239
 
237
240
  try:
238
- RasUtils.update_file(plan_file_path, RasPlan._update_unsteady_in_file, new_unsteady_flow_number)
241
+ RasUtils.update_file(plan_file_path, lambda lines: [
242
+ f"Flow File=u{new_unsteady_flow_number}\n" if line.startswith("Flow File=") else line
243
+ for line in lines
244
+ ])
245
+
246
+ # Update dataframes and additional columns
247
+ ras_obj.plan_df = ras_obj.get_plan_entries()
248
+ mask = ras_obj.plan_df['plan_number'] == plan_number
249
+ flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_flow_number}"
250
+ ras_obj.plan_df.loc[mask, 'Flow File'] = new_unsteady_flow_number
251
+ ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
252
+ ras_obj.plan_df.loc[mask, 'unsteady_number'] = new_unsteady_flow_number
253
+
254
+ # Update remaining dataframes
255
+ ras_obj.geom_df = ras_obj.get_geom_entries()
256
+ ras_obj.flow_df = ras_obj.get_flow_entries()
257
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
258
+
239
259
  except Exception as e:
240
260
  raise Exception(f"Failed to update unsteady flow file: {e}")
241
261
 
242
- # Update the ras object's dataframes
243
- ras_obj.plan_df = ras_obj.get_plan_entries()
244
- ras_obj.geom_df = ras_obj.get_geom_entries()
245
- ras_obj.flow_df = ras_obj.get_flow_entries()
246
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
247
-
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
262
  @staticmethod
253
263
  @log_call
254
264
  def set_num_cores(plan_number, num_cores, ras_object=None):
@@ -601,36 +611,31 @@ class RasPlan:
601
611
  ras_obj = ras_object or ras
602
612
  ras_obj.check_initialized()
603
613
 
604
- # Update plan entries without reinitializing the entire project
605
614
  ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
606
-
607
615
  new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
616
+
608
617
  template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
609
618
  new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
610
619
 
611
- def update_shortid(lines):
612
- shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
613
- for i, line in enumerate(lines):
614
- match = shortid_pattern.match(line.strip())
615
- if match:
616
- current_shortid = match.group(1)
617
- if new_plan_shortid is None:
618
- new_shortid = (current_shortid + "_copy")[:24]
619
- else:
620
- new_shortid = new_plan_shortid[:24]
621
- lines[i] = f"Short Identifier={new_shortid}\n"
622
- break
623
- return lines
624
-
625
- # Use RasUtils to clone the file and update the short identifier
626
- RasUtils.clone_file(template_plan_path, new_plan_path, update_shortid)
627
-
628
- # Use RasUtils to update the project file
620
+ # Clone file and update project file
621
+ RasUtils.clone_file(template_plan_path, new_plan_path,
622
+ lambda lines: [
623
+ f"Short Identifier={new_plan_shortid or (match.group(1) + '_copy')[:24]}\n"
624
+ if (match := re.match(r'^Short Identifier=(.*)$', line.strip(), re.IGNORECASE))
625
+ else line
626
+ for line in lines
627
+ ])
628
+
629
629
  RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
630
-
631
- # Re-initialize the ras global object
632
630
  ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
631
+
632
+ # Copy additional columns from template to new plan
633
+ template_row = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == template_plan].iloc[0]
634
+ for col in ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']:
635
+ if col in template_row.index:
636
+ ras_obj.plan_df.loc[ras_obj.plan_df['plan_number'] == new_plan_num, col] = template_row[col]
633
637
 
638
+ # Update all dataframes
634
639
  ras_obj.plan_df = ras_obj.get_plan_entries()
635
640
  ras_obj.geom_df = ras_obj.get_geom_entries()
636
641
  ras_obj.flow_df = ras_obj.get_flow_entries()
@@ -881,7 +886,8 @@ class RasPlan:
881
886
  'Run HTab', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
882
887
  'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores',
883
888
  'UNET Use Existing IB Tables', 'UNET 1D Methodology', 'UNET D2 Solver Type',
884
- 'UNET D2 Name', 'Run RASMapper', 'Run HTab', 'Run UNET'
889
+ 'UNET D2 Name', 'Run RASMapper', 'Run HTab', 'Run UNet', 'Run PostProcess',
890
+ 'Run WQNet', 'UNET P2 Cores', 'Run Post Process', 'Run WQNET'
885
891
  }
886
892
 
887
893
  if key not in supported_plan_keys:
@@ -1257,3 +1263,207 @@ class RasPlan:
1257
1263
  ras_object.plan_df = ras_object.get_plan_entries()
1258
1264
  ras_object.unsteady_df = ras_object.get_unsteady_entries()
1259
1265
 
1266
+ @staticmethod
1267
+ @log_call
1268
+ def get_shortid(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1269
+ """
1270
+ Get the Short Identifier from a HEC-RAS plan file.
1271
+
1272
+ Args:
1273
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1274
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1275
+
1276
+ Returns:
1277
+ str: The Short Identifier from the plan file.
1278
+
1279
+ Raises:
1280
+ ValueError: If the plan file is not found.
1281
+ IOError: If there's an error reading from the plan file.
1282
+
1283
+ Example:
1284
+ >>> shortid = RasPlan.get_shortid('01')
1285
+ >>> print(f"Plan's Short Identifier: {shortid}")
1286
+ """
1287
+ logger = get_logger(__name__)
1288
+ ras_obj = ras_object or ras
1289
+ ras_obj.check_initialized()
1290
+
1291
+ # Get the Short Identifier using get_plan_value
1292
+ shortid = RasPlan.get_plan_value(plan_number_or_path, "Short Identifier", ras_obj)
1293
+
1294
+ if shortid is None:
1295
+ logger.warning(f"Short Identifier not found in plan: {plan_number_or_path}")
1296
+ return ""
1297
+
1298
+ logger.info(f"Retrieved Short Identifier: {shortid}")
1299
+ return shortid
1300
+
1301
+ @staticmethod
1302
+ @log_call
1303
+ def set_shortid(plan_number_or_path: Union[str, Path], new_shortid: str, ras_object=None) -> None:
1304
+ """
1305
+ Set the Short Identifier in a HEC-RAS plan file.
1306
+
1307
+ Args:
1308
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1309
+ new_shortid (str): The new Short Identifier to set (max 24 characters).
1310
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1311
+
1312
+ Raises:
1313
+ ValueError: If the plan file is not found or if new_shortid is too long.
1314
+ IOError: If there's an error updating the plan file.
1315
+
1316
+ Example:
1317
+ >>> RasPlan.set_shortid('01', 'NewShortIdentifier')
1318
+ """
1319
+ logger = get_logger(__name__)
1320
+ ras_obj = ras_object or ras
1321
+ ras_obj.check_initialized()
1322
+
1323
+ # Ensure new_shortid is not too long (HEC-RAS limits short identifiers to 24 characters)
1324
+ if len(new_shortid) > 24:
1325
+ logger.warning(f"Short Identifier too long (24 char max). Truncating: {new_shortid}")
1326
+ new_shortid = new_shortid[:24]
1327
+
1328
+ # Get the plan file path
1329
+ plan_file_path = Path(plan_number_or_path)
1330
+ if not plan_file_path.is_file():
1331
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1332
+ if not plan_file_path.exists():
1333
+ logger.error(f"Plan file not found: {plan_file_path}")
1334
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1335
+
1336
+ try:
1337
+ # Read the file
1338
+ with open(plan_file_path, 'r') as file:
1339
+ lines = file.readlines()
1340
+
1341
+ # Update the Short Identifier line
1342
+ updated = False
1343
+ for i, line in enumerate(lines):
1344
+ if line.startswith("Short Identifier="):
1345
+ lines[i] = f"Short Identifier={new_shortid}\n"
1346
+ updated = True
1347
+ break
1348
+
1349
+ # If Short Identifier line not found, add it after Plan Title
1350
+ if not updated:
1351
+ for i, line in enumerate(lines):
1352
+ if line.startswith("Plan Title="):
1353
+ lines.insert(i+1, f"Short Identifier={new_shortid}\n")
1354
+ updated = True
1355
+ break
1356
+
1357
+ # If Plan Title not found either, add at the beginning
1358
+ if not updated:
1359
+ lines.insert(0, f"Short Identifier={new_shortid}\n")
1360
+
1361
+ # Write the updated content back to the file
1362
+ with open(plan_file_path, 'w') as file:
1363
+ file.writelines(lines)
1364
+
1365
+ logger.info(f"Updated Short Identifier in plan file to: {new_shortid}")
1366
+
1367
+ except IOError as e:
1368
+ logger.error(f"Error updating Short Identifier in plan file {plan_file_path}: {e}")
1369
+ raise ValueError(f"Error updating Short Identifier: {e}")
1370
+
1371
+ # Refresh RasPrj dataframes if ras_object provided
1372
+ if ras_object:
1373
+ ras_object.plan_df = ras_object.get_plan_entries()
1374
+
1375
+ @staticmethod
1376
+ @log_call
1377
+ def get_plan_title(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1378
+ """
1379
+ Get the Plan Title from a HEC-RAS plan file.
1380
+
1381
+ Args:
1382
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1383
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1384
+
1385
+ Returns:
1386
+ str: The Plan Title from the plan file.
1387
+
1388
+ Raises:
1389
+ ValueError: If the plan file is not found.
1390
+ IOError: If there's an error reading from the plan file.
1391
+
1392
+ Example:
1393
+ >>> title = RasPlan.get_plan_title('01')
1394
+ >>> print(f"Plan Title: {title}")
1395
+ """
1396
+ logger = get_logger(__name__)
1397
+ ras_obj = ras_object or ras
1398
+ ras_obj.check_initialized()
1399
+
1400
+ # Get the Plan Title using get_plan_value
1401
+ title = RasPlan.get_plan_value(plan_number_or_path, "Plan Title", ras_obj)
1402
+
1403
+ if title is None:
1404
+ logger.warning(f"Plan Title not found in plan: {plan_number_or_path}")
1405
+ return ""
1406
+
1407
+ logger.info(f"Retrieved Plan Title: {title}")
1408
+ return title
1409
+
1410
+ @staticmethod
1411
+ @log_call
1412
+ def set_plan_title(plan_number_or_path: Union[str, Path], new_title: str, ras_object=None) -> None:
1413
+ """
1414
+ Set the Plan Title in a HEC-RAS plan file.
1415
+
1416
+ Args:
1417
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1418
+ new_title (str): The new Plan Title to set.
1419
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1420
+
1421
+ Raises:
1422
+ ValueError: If the plan file is not found.
1423
+ IOError: If there's an error updating the plan file.
1424
+
1425
+ Example:
1426
+ >>> RasPlan.set_plan_title('01', 'Updated Plan Scenario')
1427
+ """
1428
+ logger = get_logger(__name__)
1429
+ ras_obj = ras_object or ras
1430
+ ras_obj.check_initialized()
1431
+
1432
+ # Get the plan file path
1433
+ plan_file_path = Path(plan_number_or_path)
1434
+ if not plan_file_path.is_file():
1435
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1436
+ if not plan_file_path.exists():
1437
+ logger.error(f"Plan file not found: {plan_file_path}")
1438
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1439
+
1440
+ try:
1441
+ # Read the file
1442
+ with open(plan_file_path, 'r') as file:
1443
+ lines = file.readlines()
1444
+
1445
+ # Update the Plan Title line
1446
+ updated = False
1447
+ for i, line in enumerate(lines):
1448
+ if line.startswith("Plan Title="):
1449
+ lines[i] = f"Plan Title={new_title}\n"
1450
+ updated = True
1451
+ break
1452
+
1453
+ # If Plan Title line not found, add it at the beginning
1454
+ if not updated:
1455
+ lines.insert(0, f"Plan Title={new_title}\n")
1456
+
1457
+ # Write the updated content back to the file
1458
+ with open(plan_file_path, 'w') as file:
1459
+ file.writelines(lines)
1460
+
1461
+ logger.info(f"Updated Plan Title in plan file to: {new_title}")
1462
+
1463
+ except IOError as e:
1464
+ logger.error(f"Error updating Plan Title in plan file {plan_file_path}: {e}")
1465
+ raise ValueError(f"Error updating Plan Title: {e}")
1466
+
1467
+ # Refresh RasPrj dataframes if ras_object provided
1468
+ if ras_object:
1469
+ ras_object.plan_df = ras_object.get_plan_entries()